۳۶ – موارد شگفت‌انگیز (Exotica)

در این، آخرین فصل سفرمان، به چند مورد پراکنده و متفرقه می‌پردازیم. اگرچه در فصل‌های قبلی مطالب زیادی را پوشش داده‌ایم، هنوز قابلیت‌های بسیاری از bash باقی مانده که درباره‌شان صحبت نکرده‌ایم. بیشتر این امکانات نسبتاً ناشناخته‌اند و عمدتاً برای کسانی مفیدند که در حال یکپارچه‌سازی bash در یک توزیع لینوکس هستند. بااین‌حال، چند مورد نیز وجود دارد که اگرچه پرکاربرد نیستند، اما برای برخی مسائل برنامه‌نویسی کمک‌کننده‌اند. در اینجا آن‌ها را بررسی خواهیم کرد.


گروه‌بندی دستورات و زیرپوسته‌ها (Group Commands and Subshells)

bash اجازه می‌دهد که چندین دستور را کنار هم گروه‌بندی کنیم. این کار به دو روش انجام می‌شود:
۱) با group command یا دستور گروهی
۲) با subshell یا زیرپوسته

نمونهٔ نحوهٔ نگارش هر دو روش:

دستور گروهی

{ command1; command2; [command3; ...] }

زیرپوسته

(command1; command2; [command3; ...])

تفاوت این دو در این است که دستور گروهی از آکولاد استفاده می‌کند و زیرپوسته از پرانتز.

نکتهٔ مهم: به دلیل شیوهٔ پیاده‌سازی گروه‌ها در bash:


کاربرد گروه‌ها و زیرپوسته‌ها چیست؟

با اینکه تفاوت مهمی میان این دو روش وجود دارد (که بعداً بیان می‌شود)، هردو برای مدیریت تغییر مسیر ورودی/خروجی (redirection) استفاده می‌شوند.

به این بخش از اسکریپت توجه کنید که خروجی چند دستور را به یک فایل هدایت می‌کند:

ls -l > output.txt
echo "Listing of foo.txt" >> output.txt
cat foo.txt >> output.txt

این روش کاملاً ساده است؛ سه دستور که خروجی همگی به فایلی به نام output.txt می‌روند.

همان کار با دستور گروهی:

{ ls -l; echo "Listing of foo.txt"; cat foo.txt; } > output.txt

و با زیرپوسته:

(ls -l; echo "Listing of foo.txt"; cat foo.txt) > output.txt

در این روش‌ها مقداری در نوشتن صرفه‌جویی می‌شود، اما قدرت واقعی آن‌ها در pipelineها مشخص می‌شود:

{ ls -l; echo "Listing of foo.txt"; cat foo.txt; } | lpr

اینجا خروجی سه دستور را ترکیب کرده‌ایم و به ورودی lpr ارسال کرده‌ایم تا یک گزارش چاپ شود.


مثال: استفاده از گروه‌ها همراه با آرایه‌های انجمنی

در اسکریپت زیر، که array-2 نام دارد، از گروه‌ها و چند تکنیک برنامه‌نویسی همراه با آرایه‌های انجمنی استفاده شده است.

این اسکریپت هنگام دریافت نام یک پوشه، فهرست فایل‌های داخل آن را همراه با نام مالک فایل و گروه مالک نمایش می‌دهد.
در پایان نیز تعداد فایل‌های متعلق به هر مالک و هر گروه را چاپ می‌کند.

خروجی نمونه (خلاصه‌شده) برای پوشهٔ /usr/bin:

(خروجی طولانی است و در متن انگلیسی آمده است، اینجا ترجمه نمی‌شود.)


کد اسکریپت با شمارهٔ خط

(کد اسکریپت همان نسخهٔ اصلی است و ترجمه نمی‌شود تا ساختار آن حفظ شود.)


توضیح مکانیزم اسکریپت

خط ۵:
آرایه‌های انجمنی باید با دستور declare و گزینهٔ -A ایجاد شوند.
در این اسکریپت ۵ آرایه ساخته می‌شود:


خطوط ۷ تا ۱۰:
بررسی می‌شود که آیا پارامتر ورودی یک پوشهٔ معتبر است. اگر نبود، پیام راهنما چاپ شده و اسکریپت با وضعیت ۱ خارج می‌شود.


خطوط ۱۲ تا ۲۰:
روی فایل‌های پوشه حلقه اجرا می‌شود. با دستور stat در خطوط ۱۳ و ۱۴ مالک و گروه فایل استخراج می‌شوند و در آرایه‌های مربوط ذخیره می‌گردند. همچنین نام فایل در آرایهٔ files ذخیره می‌شود.


خطوط ۱۸ و ۱۹:
تعداد فایل‌های متعلق به هر مالک و گروه یک واحد افزایش می‌یابد.


خطوط ۲۲ تا ۲۷:
فهرست فایل‌ها چاپ می‌شود.
از "${array[@]}" استفاده شده که عناصر آرایه را به‌صورت جداگانه گسترش می‌دهد، حتی اگر نام فایل شامل فاصله باشد.
کل حلقه در { ... } قرار گرفته تا خروجی آن بتواند مستقیماً به sort ارسال شود.


خطوط ۲۹ تا ۴۰:
دو حلقهٔ دیگر مشابه بالا، اما از "${!array[@]}" استفاده می‌کنند که به‌جای عناصر آرایه، ایندکس‌ها را گسترش می‌دهد.


جایگزینی فرآیند

در حالی که این دو (دستورات گروهی و زیربرنامه‌ها) شباهت دارند و هر دو می‌توانند برای ترکیب جریان‌ها جهت هدایت ورودی و خروجی استفاده شوند، تفاوت مهمی بین دستورات گروهی و زیربرنامه‌ها وجود دارد. در حالی که دستورات گروهی تمامی دستورات خود را در شل جاری اجرا می‌کنند، یک زیربرنامه (همانطور که از نامش پیداست) دستورات خود را در یک کپی فرزند از شل جاری اجرا می‌کند. این بدان معنی است که محیط شل کپی می‌شود و به یک نمونه جدید از شل داده می‌شود. وقتی زیربرنامه خاتمه می‌یابد، کپی محیط از دست می‌رود، بنابراین هر تغییراتی که در محیط زیربرنامه (شامل تخصیص متغیرها) انجام شود، نیز از بین می‌رود. بنابراین، در بیشتر موارد، مگر اینکه یک اسکریپت نیاز به زیربرنامه داشته باشد، دستورات گروهی نسبت به زیربرنامه‌ها ارجح هستند. دستورات گروهی هم سریع‌تر هستند و هم به حافظه کمتری نیاز دارند.

ما یک مثال از مشکل محیط زیربرنامه را در فصل ۲۸ دیدیم، زمانی که متوجه شدیم که دستور read در یک پایپ لاین به طور انتزاعی که انتظار می‌رود، عمل نمی‌کند. برای خلاصه کردن، اگر یک پایپ لاین به این شکل بسازیم:

echo "foo" | read
echo $REPLY

محتوای متغیر REPLY همیشه خالی خواهد بود زیرا دستور read در یک زیربرنامه اجرا می‌شود و کپی متغیر REPLY وقتی زیربرنامه خاتمه می‌یابد، از بین می‌رود.

زیرا دستورات در پایپ لاین‌ها همیشه در زیربرنامه‌ها اجرا می‌شوند، هر دستوری که متغیرها را تخصیص دهد با این مشکل روبرو خواهد شد. خوشبختانه، شل یک فرم خاص از گسترش به نام جایگزینی فرآیند فراهم می‌کند که می‌توان از آن برای دور زدن این مشکل استفاده کرد.

جایگزینی فرآیند به دو صورت بیان می‌شود:
برای فرایندهایی که خروجی استاندارد تولید می‌کنند:

<(list)

یا برای فرایندهایی که ورودی استاندارد دریافت می‌کنند:

>(list)

که در آن list یک لیست از دستورات است.

برای حل مشکل خود با دستور read، می‌توانیم از جایگزینی فرآیند به این شکل استفاده کنیم:

read < <(echo "foo")
echo $REPLY

جایگزینی فرآیند به ما این امکان را می‌دهد که خروجی یک زیربرنامه را به عنوان یک فایل معمولی برای اهداف هدایت ورودی/خروجی در نظر بگیریم. در واقع، از آنجا که این یک فرم گسترش است، می‌توانیم ارزش واقعی آن را بررسی کنیم:

[me@linuxbox ~]$ echo <(echo "foo")
/dev/fd/63

با استفاده از دستور echo برای مشاهده نتیجه گسترش، می‌بینیم که خروجی زیربرنامه توسط فایلی به نام /dev/fd/63 تأمین می‌شود.

جایگزینی فرآیند اغلب با حلقه‌هایی که شامل دستور read هستند استفاده می‌شود. در اینجا یک مثال از یک حلقه read است که محتوای یک لیست دایرکتوری تولید شده توسط زیربرنامه را پردازش می‌کند:

#!/bin/bash
# pro-sub : demo of process substitution
while read attr links owner group size date time filename; do
cat <<- EOF
Filename:   $filename
Size:       $size
Owner:      $owner
Group:      $group
Modified:   $date $time
Links:      $links
Attributes: $attr
EOF
done < <(ls -l | tail -n +2)

این حلقه برای هر خط از لیست دایرکتوری دستور read را اجرا می‌کند. خود لیست دایرکتوری در خط نهایی اسکریپت تولید می‌شود. این خط خروجی جایگزینی فرآیند را به ورودی استاندارد حلقه هدایت می‌کند. دستور tail در پایپ لاین جایگزینی فرآیند برای حذف اولین خط لیست گنجانده شده است که به آن نیازی نیست.

هنگامی که اسکریپت اجرا می‌شود، خروجی به این شکل خواهد بود:

[me@linuxbox ~]$ pro_sub | head -n 20
Filename:   addresses.ldif
Size:       14540
Owner:      me
Group:      me
Modified:   2009-04-02 11:12
Links:      1
Attributes: -rw-r--r--
Filename:   bin
Size:       4096
Owner:      me
Group:      me
Modified:   2009-07-10 07:31
Links:      2
Attributes: drwxr-xr-x
Filename:   bookmarks.html
Size:       394213
Owner:      me
Group:      me

گرفتارها

در فصل ۱۰ دیدیم که چگونه برنامه‌ها می‌توانند به سیگنال‌ها پاسخ دهند. ما می‌توانیم این قابلیت را به اسکریپت‌های خود نیز اضافه کنیم. در حالی که اسکریپت‌هایی که تاکنون نوشته‌ایم به این قابلیت نیاز نداشته‌اند (چرا که زمان اجرای آن‌ها بسیار کوتاه است و فایل‌های موقت ایجاد نمی‌کنند)، اسکریپت‌های بزرگتر و پیچیده‌تر ممکن است از داشتن یک روال برای مدیریت سیگنال‌ها بهره‌مند شوند.

هنگام طراحی یک اسکریپت بزرگ و پیچیده، مهم است که در نظر بگیریم چه اتفاقی می‌افتد اگر کاربر در حین اجرای اسکریپت، از سیستم خارج شود یا کامپیوتر را خاموش کند. وقتی چنین اتفاقی رخ دهد، سیگنالی به تمامی فرایندهای مربوطه ارسال خواهد شد. در نتیجه، برنامه‌ها می‌توانند اقداماتی برای تضمین خاتمه مناسب و مرتب برنامه انجام دهند. برای مثال، فرض کنید اسکریپتی نوشته‌ایم که در حین اجرای خود یک فایل موقت ایجاد می‌کند. در طراحی خوب، اسکریپت باید فایل را زمانی که کار خود را تمام کرد حذف کند. همچنین، بهتر است که اسکریپت فایل را اگر سیگنالی دریافت کند که نشان دهد برنامه به طور پیش‌هنگام قرار است خاتمه یابد، حذف کند.

بش ارائه می‌دهد یک مکانیزم به نام "گرفتار" برای این منظور. دستور trap برای پیاده‌سازی این مکانیزم استفاده می‌شود:

trap argument signal [signal...]

که در آن argument یک رشته است که به عنوان دستور خوانده و تفسیر می‌شود و signal مشخص‌کننده سیگنالی است که اجرای دستور تفسیر شده را تحریک می‌کند.

این یک مثال ساده است:

#!/bin/bash
# trap-demo : simple signal handling demo
trap "echo 'I am ignoring you.'" SIGINT SIGTERM
for i in {1..5}; do
echo "Iteration $i of 5"
sleep 5
done

این اسکریپت یک گیرنده تعریف می‌کند که هر بار که سیگنال‌های SIGINT یا SIGTERM دریافت شوند، دستور echo را اجرا می‌کند. زمانی که کاربر تلاش می‌کند اسکریپت را با فشار دادن Ctrl-c متوقف کند، برنامه به این شکل اجرا می‌شود:

[me@linuxbox ~]$ trap-demo
Iteration 1 of 5
Iteration 2 of 5
I am ignoring you.
Iteration 3 of 5
I am ignoring you.
Iteration 4 of 5
Iteration 5 of 5

همانطور که می‌بینیم، هر بار که کاربر سعی می‌کند برنامه را قطع کند، پیام "I am ignoring you." چاپ می‌شود.

ساختن یک رشته برای ایجاد یک توالی مفید از دستورات ممکن است ناخوشایند باشد، بنابراین معمولاً از یک تابع شل برای دستور استفاده می‌شود. در این مثال، یک تابع شل جداگانه برای هر سیگنال مشخص می‌شود:

#!/bin/bash
# trap-demo2 : simple signal handling demo
exit_on_signal_SIGINT () {
echo "Script interrupted." 2>&1
exit 0
}
exit_on_signal_SIGTERM () {
echo "Script terminated." 2>&1
exit 0
}
trap exit_on_signal_SIGINT SIGINT
trap exit_on_signal_SIGTERM SIGTERM
for i in {1..5}; do
echo "Iteration $i of 5"
sleep 5
done

این اسکریپت دو دستور trap دارد، یکی برای هر سیگنال. هر دستور trap به نوبه خود یک تابع شل را مشخص می‌کند که هنگام دریافت سیگنال خاص اجرا می‌شود. توجه داشته باشید که دستور exit در هر یک از توابع مدیریت سیگنال گنجانده شده است. بدون دستور exit، اسکریپت بعد از اتمام تابع ادامه خواهد یافت.

هنگامی که کاربر در حین اجرای این اسکریپت Ctrl-c را فشار می‌دهد، نتیجه به این شکل خواهد بود:

[me@linuxbox ~]$ trap-demo2
Iteration 1 of 5
Iteration 2 of 5
Script interrupted.

فایل‌های موقت

یکی از دلایلی که چرا گیرنده‌های سیگنال در اسکریپت‌ها گنجانده می‌شوند، حذف فایل‌های موقتی است که اسکریپت ممکن است برای نگهداری نتایج موقت در حین اجرا ایجاد کند. یک هنر در نامگذاری فایل‌های موقت وجود دارد. به طور سنتی، برنامه‌ها در سیستم‌های مشابه یونیکس فایل‌های موقت خود را در دایرکتوری /tmp ایجاد می‌کنند که دایرکتوری مشترک برای چنین فایل‌هایی است. با این حال، از آنجا که این دایرکتوری مشترک است، این موضوع نگرانی‌های امنیتی ایجاد می‌کند، به ویژه برای برنامه‌هایی که با دسترسی‌های فوق‌العاده اجرا می‌شوند.

به جز مرحله آشکار تنظیم مجوزهای صحیح برای فایل‌هایی که برای همه کاربران سیستم در دسترس هستند، مهم است که فایل‌های موقت نام‌هایی غیرقابل پیش‌بینی داشته باشند. این از یک حمله به نام "حمله مسابقه فایل‌های موقت" جلوگیری می‌کند. یکی از راه‌ها برای ایجاد یک نام غیرقابل پیش‌بینی (اما همچنان توصیفی) این است که چیزی مانند این انجام دهیم:

tempfile=/tmp/$(basename $0).$$.$RANDOM

این نامی متشکل از نام برنامه، به دنبال آن شناسه فرآیند (PID)، و سپس یک عدد تصادفی ایجاد می‌کند. با این حال، توجه داشته باشید که متغیر شل $RANDOM تنها مقداری در محدوده ۱ تا ۳۲۷۶۷ برمی‌گرداند که در شرایط رایانه‌ای دامنه زیادی نیست، بنابراین استفاده از یک نمونه از این متغیر برای غلبه بر یک مهاجم مصمم کافی نیست.

راه بهتر این است که از برنامه mktemp استفاده کنید (نه با تابع کتابخانه‌ای mktemp اشتباه بگیرید) تا هم نامگذاری و هم ایجاد فایل موقت را انجام دهید. برنامه mktemp یک الگو را به عنوان آرگومان می‌پذیرد که برای ساخت نام فایل استفاده می‌شود. الگو باید شامل یک سری از کاراکترهای "X" باشد که با حروف و اعداد تصادفی معادل جایگزین می‌شود. هرچه تعداد کاراکترهای "X" بیشتر باشد، رشته تصادفی نیز طولانی‌تر خواهد بود. در اینجا یک مثال است:

tempfile=$(mktemp /tmp/foobar.$$.XXXXXXXXXX)

این یک فایل موقت ایجاد می‌کند و نام آن را به متغیر tempfile اختصاص می‌دهد. کاراکترهای "X" در الگو با حروف و اعداد تصادفی جایگزین می‌شوند به طوری که نام نهایی فایل (که در این مثال همچنین شامل مقدار گسترش یافته پارامتر ویژه $$ برای دریافت PID است) ممکن است چیزی مانند این باشد:

/tmp/foobar.6593.UOZuvM6654

برای اسکریپت‌هایی که توسط کاربران عادی اجرا می‌شوند، ممکن است بهتر باشد از دایرکتوری /tmp استفاده نکنید و یک دایرکتوری برای فایل‌های موقت در دایرکتوری خانه کاربر ایجاد کنید، با خط کدی مانند این:

[[ -d $HOME/tmp ]] || mkdir $HOME/tmp

اجرای همزمان

گاهی اوقات ممکن است بخواهیم چندین کار را به طور همزمان انجام دهیم. ما قبلاً دیدیم که تمام سیستم‌عامل‌های مدرن حداقل چندوظیفه‌ای هستند و در بسیاری از موارد چندکاربره نیز می‌باشند. اسکریپت‌ها می‌توانند به گونه‌ای نوشته شوند که به صورت چندوظیفه‌ای عمل کنند.

معمولاً این به این صورت است که یک اسکریپت پدر اجرا می‌شود که به نوبه خود یک یا چند اسکریپت فرزند را راه‌اندازی می‌کند که وظیفه اضافی را انجام می‌دهند در حالی که اسکریپت پدر به اجرای خود ادامه می‌دهد. با این حال، زمانی که یک سری اسکریپت‌ها به این صورت اجرا می‌شوند، ممکن است مشکلاتی در هماهنگی پدر و فرزند به وجود آید. به این معنی که اگر اسکریپت پدر یا فرزند به دیگری وابسته باشد، ممکن است یکی از اسکریپت‌ها مجبور باشد منتظر بماند تا دیگری کار خود را تمام کند.

بش یک دستور داخلی به نام wait دارد که به مدیریت اجرای همزمان کمک می‌کند. دستور wait باعث می‌شود اسکریپت پدر تا زمانی که یک فرآیند مشخص (یعنی اسکریپت فرزند) تمام شود، متوقف شود.

در اینجا ابتدا نحوه عملکرد دستور wait را نشان می‌دهیم. برای این کار به دو اسکریپت نیاز داریم: یک اسکریپت پدر:

#!/bin/bash
# async-parent : Asynchronous execution demo (parent)
echo "Parent: starting..."
echo "Parent: launching child script..."
async-child &
pid=$!
echo "Parent: child (PID= $pid) launched."
echo "Parent: continuing..."
sleep 2
echo "Parent: pausing to wait for child to finish..."
wait $pid
echo "Parent: child is finished. Continuing..."
echo "Parent: parent is done. Exiting."

و یک اسکریپت فرزند:

#!/bin/bash
# async-child : Asynchronous execution demo (child)
echo "Child: child is running..."
sleep 5
echo "Child: child is done. Exiting."

در این مثال، اسکریپت فرزند بسیار ساده است. عمل اصلی در اسکریپت پدر انجام می‌شود. در اسکریپت پدر، اسکریپت فرزند راه‌اندازی می‌شود و در پس‌زمینه قرار می‌گیرد. شناسه فرآیند (PID) اسکریپت فرزند با مقدار پارامتر شل $! که همیشه شناسه آخرین فرآیند پس‌زمینه را نشان می‌دهد، ذخیره می‌شود.

اسکریپت پدر به اجرا ادامه می‌دهد و سپس دستور wait را با PID فرآیند فرزند اجرا می‌کند. این باعث می‌شود که اسکریپت پدر تا زمانی که اسکریپت فرزند تمام شود، متوقف شود و پس از آن اسکریپت پدر به اتمام می‌رسد.

هنگام اجرای اسکریپت‌های پدر و فرزند، خروجی به صورت زیر خواهد بود:

[me@linuxbox ~]$ async-parent
Parent: starting...
Parent: launching child script...
Parent: child (PID= 6741) launched.
Parent: continuing...
Child: child is running...
Parent: pausing to wait for child to finish...
Child: child is done. Exiting.
Parent: child is finished. Continuing...
Parent: parent is done. Exiting.

لوله‌های نام‌دار

در اکثر سیستم‌های شبیه یونیکس، امکان ایجاد نوع خاصی از فایل به نام لوله‌های نام‌دار (named pipes) وجود دارد. لوله‌های نام‌دار برای ایجاد ارتباط بین دو فرآیند استفاده می‌شوند و مانند سایر انواع فایل‌ها عمل می‌کنند. آن‌ها خیلی رایج نیستند اما دانستن آن‌ها مفید است.

یک معماری برنامه‌نویسی معمول به نام "کلاینت-سرور" وجود دارد که می‌تواند از روش‌های ارتباطی مانند لوله‌های نام‌دار، و همچنین سایر روش‌های ارتباط بین فرآیندها مانند ارتباطات شبکه‌ای استفاده کند.

رایج‌ترین نوع سیستم کلاینت-سرور، البته، یک مرورگر وب است که با یک سرور وب ارتباط برقرار می‌کند. مرورگر وب به عنوان کلاینت عمل می‌کند و درخواست‌هایی به سرور ارسال می‌کند و سرور در پاسخ به مرورگر، صفحات وب را ارسال می‌کند.

لوله‌های نام‌دار مانند فایل‌ها عمل می‌کنند، اما در واقع بافرهای FIFO (اولین ورودی، اولین خروجی) را تشکیل می‌دهند. همانطور که در لوله‌های معمولی (بدون نام) داده از یک طرف وارد می‌شود و از طرف دیگر خارج می‌شود، در لوله‌های نام‌دار نیز این عمل مشابه است. با لوله‌های نام‌دار، می‌توان این‌طور تنظیم کرد:

process1 > named_pipe

و

process2 < named_pipe

و این عمل به گونه‌ای رفتار می‌کند که گویی:

process1 | process2

راه‌اندازی لوله نام‌دار

ابتدا باید یک لوله نام‌دار ایجاد کنیم. این کار با استفاده از دستور mkfifo انجام می‌شود:

[me@linuxbox ~]$ mkfifo pipe1
[me@linuxbox ~]$ ls -l pipe1
prw-r--r-- 1 me me 0 2009-07-17 06:41 pipe1

در اینجا با استفاده از دستور mkfifo یک لوله نام‌دار به نام pipe1 ایجاد کرده‌ایم. با استفاده از دستور ls فایل را بررسی کرده و می‌بینیم که اولین حرف در بخش ویژگی‌ها "p" است که نشان می‌دهد این یک لوله نام‌دار است.

استفاده از لوله‌های نام‌دار

برای نشان دادن نحوه کارکرد لوله نام‌دار، به دو پنجره ترمینال (یا به طور متناوب، دو کنسول مجازی) نیاز داریم. در ترمینال اول، یک دستور ساده وارد کرده و خروجی آن را به لوله نام‌دار هدایت می‌کنیم:

[me@linuxbox ~]$ ls -l > pipe1

پس از فشردن کلید Enter، دستور به نظر می‌رسد که متوقف شده است. این به این دلیل است که هنوز هیچ چیزی از طرف دیگر لوله داده‌ها را دریافت نکرده است. هنگامی که این اتفاق می‌افتد، گفته می‌شود که لوله مسدود شده است. این وضعیت زمانی برطرف می‌شود که یک فرآیند به طرف دیگر متصل شود و شروع به خواندن داده‌ها از لوله کند. در پنجره ترمینال دوم، این دستور را وارد می‌کنیم:

[me@linuxbox ~]$ cat < pipe1

و فهرست دایرکتوری تولید شده از ترمینال اول به عنوان خروجی دستور cat در ترمینال دوم ظاهر می‌شود. دستور ls در ترمینال اول زمانی که دیگر مسدود نباشد، به پایان می‌رسد.

جمع‌بندی

خب، سفر ما تمام شد. تنها چیزی که باقی‌مانده این است که تمرین کنیم، تمرین کنیم، تمرین کنیم. اگرچه ما در این سفر به مطالب زیادی پرداخته‌ایم، اما فقط سطح دستورات خط فرمان را خراشیده‌ایم. هنوز هزاران برنامه خط فرمان وجود دارند که می‌توانید آن‌ها را کشف کنید و از آن‌ها لذت ببرید. کافی است در دایرکتوری /usr/bin کاوش کنید و خواهید دید!

مطالعه بیشتر