آموزش Bash Script ویندوز 0 تا 100

اگر روی ویندوز کار می‌کنید و می‌خواهید Bash Script یاد بگیرید، مهم‌ترین نکته این است که Bash را با PowerShell، Command Prompt یا حتی خود Linux یکی ندانید. Bash یک Shell و زبان اسکریپت‌نویسی است؛ در ویندوز معمولاً آن را داخل محیطی مانند Windows Subsystem for Linux یا WSL و Git Bash اجرا می‌کنیم. به همین دلیل آموزش Bash در Windows فقط یادگیری دستورهایی مثل if، for و grep نیست؛ باید بدانید اسکریپت در کدام محیط اجرا می‌شود، مسیر فایل‌ها چگونه ترجمه می‌شود، تفاوت LF و CRLF چیست و چرا اسکریپتی که روی Linux سالم است ممکن است در Windows خطا بدهد.این آموزش از صفر شروع می‌کند و تا ساخت یک اسکریپت کاربردی پیش می‌رود. ابتدا WSL و Git Bash را مقایسه می‌کنیم، سپس ساختار Bash Script، متغیرها، ورودی‌ها، شرط‌ها، حلقه‌ها، Function، Array، Pipe و پردازش متن را یاد می‌گیریم. بعد سراغ بخش‌هایی می‌رویم که بیشتر آموزش‌های عمومی Bash کمتر به آن‌ها توجه می‌کنند: Pathهای ویندوز، فایل‌سیستم WSL، Line Ending، Permission، ShellCheck، Debugging و قابلیت انتقال اسکریپت بین Windows و Linux. هدف این است که در پایان فقط چند دستور حفظ نکرده باشید؛ بلکه بتوانید اسکریپتی قابل فهم، قابل عیب‌یابی و مناسب اتوماسیون واقعی بنویسید.اگر هدف شما مدیریت عمیق سرویس‌ها، Registry، Active Directory یا اجزای بومی ویندوز است، Bash همیشه بهترین گزینه نیست و آموزش PowerShell از صفر تا صد مسیر مناسب‌تری خواهد بود. اما برای Git، ابزارهای GNU/Linux، Server، DevOps، پردازش فایل و متن، SSH، پروژه‌های چندسکویی و بسیاری از workflowهای توسعه، Bash مهارتی بسیار ارزشمند است.

Bash در ویندوز چیست؟

برای شروع باید چهار مفهوم را از هم جدا کنیم: Terminal، Shell، Bash و Linux. Terminal برنامه یا محیطی است که ورودی و خروجی خط فرمان را نمایش می‌دهد. Shell برنامه‌ای است که فرمان‌های شما را تفسیر می‌کند. Bash یکی از Shellهاست و علاوه بر اجرای فرمان‌های تعاملی، زبان اسکریپت‌نویسی خودش را دارد. Linux نیز سیستم‌عامل و اکوسیستمی است که Bash در آن بسیار رایج است، اما Bash به معنای Linux نیست.

Shell، Terminal و Bash

در Windows Terminal می‌توانید چند Shell مختلف را باز کنید؛ مثلاً PowerShell، Command Prompt یا یک توزیع WSL که Bash را اجرا می‌کند. Windows Terminal خودش زبان اسکریپت‌نویسی نیست. به همین ترتیب، باز کردن Git Bash یعنی شما یک محیط Terminal/Console در اختیار دارید که Bash و مجموعه‌ای از ابزارهای Unix-like را برای Windows فراهم می‌کند.

آشنایی با تفاوت Shell، Terminal و Bash در ویندوز و نحوه اجرای PowerShell، Command Prompt، WSL و Git Bash برای اسکریپت‌نویسی
تفاوت Shell، Terminal و Bash در ویندوز مشخص می‌کند هر فرمان باید در PowerShell، Git Bash یا محیط WSL چگونه اجرا شود.

این تفکیک در عیب‌یابی اهمیت زیادی دارد. وقتی فرمانی در Git Bash کار می‌کند اما در PowerShell نه، لزوماً مشکلی در Windows وجود ندارد؛ ممکن است آن فرمان متعلق به ابزارهای Unix باشد. برعکس، یک Cmdlet پاورشل مثل Get-Service بخشی از Bash نیست. بنابراین همیشه قبل از اجرای یک دستور مشخص کنید در کدام Shell هستید.

Bash Script چه کاری می‌کند؟

Shell Script یک فایل متنی شامل فرمان‌هایی است که Shell آن‌ها را به‌ترتیب می‌خواند و اجرا می‌کند. در Bash می‌توانید علاوه بر اجرای برنامه‌ها، متغیر بسازید، ورودی بگیرید، شرط تعریف کنید، روی فایل‌ها حلقه بزنید، Function بنویسید، خروجی برنامه‌ها را با Pipe به برنامه دیگری بدهید و بر اساس Exit Status تصمیم بگیرید. همین ترکیب ساده، Bash را برای اتوماسیون کارهای تکراری قدرتمند می‌کند.

سناریوهای مناسب شامل تغییر نام یا دسته‌بندی فایل‌ها، Backup، آماده‌سازی محیط توسعه، اجرای چند ابزار پشت سر هم، پردازش Log، بررسی وضعیت فایل‌ها، کنترل Git repository، اجرای command روی Server و ساخت اسکریپت‌های CI/CD است. برای مبانی گسترده‌تر سیستم‌عامل و command line می‌توانید در کنار این مقاله از آموزش ویندوز صفر تا صد و آموزش لینوکس صفر تا صد استفاده کنید.

انتخاب محیط Bash

در ویندوز یک «Bash واحد» وجود ندارد که برای همه سناریوها بهترین باشد. انتخاب محیط روی مسیر فایل‌ها، ابزارهای قابل دسترس، Permission، Package Manager و میزان شباهت رفتار اسکریپت به Linux اثر می‌گذارد. برای بیشتر کاربران امروزی، تصمیم اصلی بین WSL و Git Bash است؛ MSYS2 و Cygwin معمولاً زمانی مطرح می‌شوند که نیاز تخصصی‌تری دارید.

جدول زیر انتخاب را ساده می‌کند. این مقایسه به معنی برنده مطلق نیست؛ معیار اصلی این است که اسکریپت را برای چه کاری می‌خواهید.

محیطمناسب برایمحدودیت اصلی
WSL 2یادگیری Bash نزدیک به Linux، DevOps، Server، ابزارهای Linux و پروژه‌های چندسکویییک محیط Linux جدا دارد و باید تفاوت فایل‌سیستم Windows/WSL را بفهمید
Git BashGit، فرمان‌های Unix-like، اسکریپت‌های سبک تا متوسط و کار سریع داخل پوشه‌های Windowsیک Linux کامل نیست و همه Package/Serviceهای Linux را ارائه نمی‌کند
MSYS2 / Cygwinنیازهای سازگاری POSIX، toolchain و سناریوهای تخصصیپیچیدگی بیشتر و معمولاً نامناسب برای شروع عمومی

اگر هدف اصلی شما یادگیری Bash به شکلی است که بعداً روی Ubuntu، Linux Server، Docker یا CI/CD هم قابل استفاده باشد، WSL معمولاً مسیر آموزشی مستقیم‌تری است. اگر بیشتر با Git و فایل‌های موجود روی Windows کار می‌کنید و نمی‌خواهید یک محیط Linux کامل داشته باشید، Git Bash سبک‌تر و سریع‌تر وارد workflow روزانه می‌شود.

WSL برای چه کسی؟

Windows Subsystem for Linux یا WSL به شما اجازه می‌دهد یک توزیع GNU/Linux را در کنار Windows اجرا کنید. در WSL، Bash داخل یک محیط Linux اجرا می‌شود و ابزارهایی مثل Package Manager توزیع، Permissionهای لینوکسی، ساختار مسیرهای Linux و بسیاری از utilityهای استاندارد را در اختیار دارید. برای کسی که هدفش Bash واقعی در کنار ابزارهای Linux است، این ویژگی مهم‌تر از آن است که فقط چند command مشابه Unix داشته باشد.

WSL همچنین برای توسعه وب، Python، Node.js، Git، SSH، Docker-related workflows و کار با Serverها مناسب است. البته باید یاد بگیرید پروژه‌تان روی فایل‌سیستم Linux است یا روی درایو Windows mount شده؛ این تفاوت هم از نظر Path و هم در برخی workloadها از نظر Performance اهمیت دارد.

Git Bash برای چه کسی؟

Git for Windows همراه خود محیط Git Bash را ارائه می‌کند. این محیط Bash و مجموعه‌ای از ابزارهای سازگار با سبک Unix را با لایه سازگاری MSYS2 در Windows فراهم می‌کند. اگر قبلاً Git for Windows نصب کرده باشید، احتمالاً Git Bash را هم دارید و می‌توانید بدون نصب یک Linux distribution شروع به تمرین Bash کنید.

Git Bash برای فرمان‌هایی مانند git، ssh، grep، find و بسیاری از scriptهای معمول بسیار کاربردی است. اما نباید آن را دقیقاً معادل Ubuntu یا یک Linux Server در نظر بگیرید؛ رفتار بعضی pathها، ابزارها، symbolic linkها و integration با Windows متفاوت است. اگر اسکریپت شما به packageهای Linux، systemd یا رفتار دقیق filesystem لینوکسی وابسته باشد، WSL انتخاب مطمئن‌تری است.

کدام محیط را انتخاب کنیم؟

برای همین آموزش می‌توانید با هر دو محیط جلو بروید، اما اگر هیچ محدودیت خاصی ندارید، WSL 2 را به‌عنوان محیط اصلی یادگیری Bash در نظر بگیرید و Git Bash را هم بشناسید. این ترکیب کمک می‌کند هم رفتار Bash در محیط نزدیک به Linux را یاد بگیرید و هم بدانید روی پوشه‌های Windows چه تفاوت‌هایی خواهید دید.

مقایسه محیط‌های WSL 2، Git Bash، PowerShell و MSYS2 برای آموزش Bash Script ویندوز و انتخاب ابزار مناسب برنامه‌نویسی و DevOps
انتخاب محیط مناسب برای Bash در ویندوز به کاربرد بستگی دارد؛ WSL 2 برای یادگیری عمیق، Git Bash برای کارهای سبک و PowerShell برای مدیریت ویندوز.
  • WSL را انتخاب کنید اگر هدف شما Linux Server، DevOps، Docker، SSH، توسعه چندسکویی یا یادگیری عمیق Bash است.
  • Git Bash را انتخاب کنید اگر Git و command-line سبک روی فایل‌های Windows نیاز اصلی شماست.
  • PowerShell را جدی بگیرید اگر بیشتر کارها Windows-native هستند؛ سرویس‌ها، Registry، CIM/WMI، Microsoft 365 و مدیریت ساختاریافته Objectها از نقاط قوت آن هستند.
  • MSYS2 یا Cygwin را بعداً بررسی کنید اگر دقیقاً می‌دانید چه compatibility/toolchain خاصی از آن‌ها می‌خواهید.

نصب و آماده‌سازی

در سال 2026 روش استاندارد Microsoft برای نصب WSL روی Windows 11 و نسخه‌های سازگار Windows 10 استفاده از دستور wsl –install است. اگر سیستم شما جدید و به‌روز باشد، معمولاً نیازی نیست مراحل قدیمی فعال‌سازی دستی Featureها را دنبال کنید. برای سیستم‌های قدیمی‌تر، روش manual installation وجود دارد، اما این مقاله مسیر جاری و ساده را مبنا قرار می‌دهد.

پیش‌نیازهای این آموزش سنگین نیستند. لازم نیست قبلاً Linux Administrator یا برنامه‌نویس حرفه‌ای باشید، اما باید بتوانید یک Terminal باز کنید، مسیر پوشه‌ها را تشخیص دهید و فایل متنی بسازید. بهتر است برای تمرین یک پوشه جدا با فایل‌های آزمایشی داشته باشید تا هیچ command حذف، Move یا overwrite را روی داده مهم امتحان نکنید.

  • Windows 11 به‌روز برای مسیر پیشنهادی WSL؛ اگر روی نسخه قدیمی‌تری از Windows هستید، ابتدا سازگاری WSL را بررسی کنید.
  • یک Editor مانند VS Code یا هر ویرایشگری که Encoding و Line Ending را واضح نمایش دهد.
  • دسترسی معمول کاربر برای تمرین Bash؛ فقط مرحله نصب WSL ممکن است به Administrator نیاز داشته باشد.
  • یک پوشه Test با فایل‌های غیرحساس برای تمرین Copy، Move، Find و Loop.
  • آشنایی بسیار پایه با مفهوم File، Folder و Path؛ بقیه مفاهیم Bash در همین مسیر ساخته می‌شوند.

در طول آموزش commandها را صرفاً Copy/Paste نکنید. قبل از Enter زدن مشخص کنید command در WSL اجرا می‌شود، Git Bash یا PowerShell. همین عادت ساده جلوی بخش بزرگی از سردرگمی‌های Windows shell را می‌گیرد. اگر prompt یا path برایتان مبهم است، ابتدا pwd و سپس printf ‘%s\n’ “$SHELL” را اجرا کنید تا context روشن‌تر شود.

نصب WSL و Bash

PowerShell را با دسترسی Administrator باز کنید و دستور زیر را اجرا کنید:

wsl --install

پس از پایان نصب و Restart در صورت درخواست سیستم، توزیع Linux اولیه راه‌اندازی می‌شود و از شما نام کاربری و رمز عبور Linux می‌خواهد. این حساب مستقل از حساب Windows است. برای دیدن توزیع‌های نصب‌شده و نسخه WSL آن‌ها می‌توانید دستور زیر را در PowerShell یا Command Prompt اجرا کنید:

wsl -l -v

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

wsl --list --online

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

wsl --install -d Ubuntu

بعد از ورود به Bash داخل WSL، وجود Bash را بررسی کنید:

bash --version
printf '%s\n' "$SHELL"

در این مرحله لازم نیست شماره نسخه خاصی را هدف قرار دهید. مهم این است که Scriptهای مقاله را با Bash اجرا کنید، نه اینکه ناخواسته آن‌ها را به Shell دیگری بدهید.

نصب Git Bash و ابزارها

Git Bash همراه Git for Windows نصب می‌شود. پس از نصب Git for Windows، از Start Menu عبارت Git Bash را باز کنید و با دو فرمان زیر محیط را بررسی کنید:

git --version
bash --version

Windows Terminal نیز می‌تواند برای مدیریت چند محیط خط فرمان کنار هم مفید باشد؛ می‌توانید PowerShell و پروفایل‌های WSL را در Tabهای جدا باز کنید. Git Bash هم بسته به پیکربندی می‌تواند از Terminal مناسب خود یا integrationهای دیگر استفاده کند. برای ویرایش فایل‌های Bash، VS Code یا هر Editor دیگری که Line Ending را واضح نمایش دهد مناسب است.

قبل از شروع یک پوشه تمرینی بسازید. در WSL بهتر است اگر عمده کار با ابزارهای Linux انجام می‌شود، پروژه را در home لینوکسی نگه دارید. برای مثال:

mkdir -p ~/projects/bash-lab
cd ~/projects/bash-lab
pwd

اگر در Git Bash کار می‌کنید، پوشه‌ای داخل Windows مانند C:\Users\YourName\Projects\bash-lab نیز طبیعی است. در بخش تفاوت فایل‌سیستم‌ها توضیح می‌دهیم چرا محل پروژه در WSL اهمیت دارد.

اولین Bash Script

یک Bash Script در ساده‌ترین حالت فایل متنی شامل commandهای Shell است. پسوند .sh برای شناسایی و سازمان‌دهی بسیار رایج و مفید است، اما خود Bash برای خواندن Script به وجود این پسوند وابسته نیست. مهم‌تر از پسوند، محتوای فایل و روشی است که آن را اجرا می‌کنید.

ساخت فایل Script

فایلی به نام hello.sh بسازید و این محتوا را داخل آن قرار دهید:

#!/usr/bin/env bash
printf '%s\n' "Hello from Bash"
printf 'User: %s\n' "$USER"
printf 'Directory: %s\n' "$PWD"

خط اول Shebang است و به سیستم می‌گوید هنگام اجرای مستقیم فایل، Interpreter مناسب را پیدا کند. استفاده از /usr/bin/env bash در بسیاری از محیط‌ها انعطاف بیشتری نسبت به فرض کردن یک مسیر ثابت برای Bash دارد، هرچند در محیط‌هایی که سیاست Deployment دقیق دارند ممکن است مسیر مشخص Interpreter ترجیح داده شود.

برای اجرای Script بدون وابستگی به executable bit می‌توانید مستقیماً Bash را فراخوانی کنید:

bash hello.sh

در این حالت Bash فایل را می‌خواند و اجرا می‌کند. این روش برای شروع و عیب‌یابی بسیار شفاف است.

Shebang و اجرای فایل

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

chmod +x hello.sh
./hello.sh

تفاوت این دو روش مهم است. فرمان bash hello.sh صراحتاً Bash را اجرا می‌کند و نام فایل را به آن می‌دهد. فرمان ./hello.sh خود فایل را به‌عنوان executable اجرا می‌کند و Shebang برای انتخاب Interpreter نقش پیدا می‌کند. وقتی با خطای Shebang، Permission یا Line Ending روبه‌رو می‌شوید، تشخیص اینکه کدام روش را استفاده کرده‌اید اولین قدم عیب‌یابی است.

خروجی و Exit Code

هر command در Shell یک وضعیت خروج یا Exit Status برمی‌گرداند. به‌طور قراردادی مقدار صفر نشان‌دهنده موفقیت است و مقدار غیرصفر خطا یا وضعیت خاص دیگری را نشان می‌دهد. مقدار خروج آخرین command را می‌توانید با $? ببینید:

bash hello.sh
printf 'Exit code: %s\n' "$?"

در Script می‌توانید خودتان با exit وضعیت نهایی را مشخص کنید:

#!/usr/bin/env bash
if [[ ! -d "$HOME" ]]; then
  printf '%s\n' "Home directory not found" >&2
  exit 1
fi
printf '%s\n' "Environment looks OK"
exit 0

در اتوماسیون، Exit Code بسیار مهم است؛ Task Scheduler، CI/CD، Script والد یا ابزار مانیتورینگ می‌تواند بر اساس آن بفهمد عملیات موفق بوده یا نه. بنابراین Script حرفه‌ای فقط پیام «Error» چاپ نمی‌کند؛ وضعیت قابل پردازش نیز برمی‌گرداند.

همین اولین Script جای مناسبی برای یادگیری یک عادت دیگر است: تفاوت «اجرا در Shell فعلی» و «اجرا به‌عنوان process جدا». وقتی bash hello.sh را اجرا می‌کنید، Bash جدید فایل را پردازش می‌کند و بعد پایان می‌یابد. اگر داخل Script با cd پوشه را عوض کنید، بعد از پایان Script معمولاً Directory Shell والد شما تغییر نمی‌کند. برای تغییر محیط Shell جاری از مفهوم source یا . استفاده می‌شود، اما این کار باید آگاهانه باشد؛ چون Script می‌تواند متغیرها، Functionها و Directory همان Session را تغییر دهد.

به همین دلیل فایل‌های setup مثل بعضی تنظیمات Shell با source بارگذاری می‌شوند، اما Scriptهای اتوماسیون عمومی بهتر است مستقل اجرا شوند. استقلال process باعث می‌شود side effectها کنترل‌پذیرتر باشند. اگر روزی دیدید Script شما فقط زمانی «کار می‌کند» که با source script.sh اجرا شود، بررسی کنید آیا واقعاً هدف تغییر محیط Shell جاری است یا ناخواسته به state بیرونی وابسته شده‌اید.

متغیرها و ورودی‌ها

اسکریپتی که همه مسیرها و مقدارها را داخل کد ثابت نوشته باشد، خیلی زود محدود می‌شود. متغیرها، Environment Variableها و Argumentها کمک می‌کنند یک Script را برای چند فایل، چند مسیر و چند حالت استفاده کنید. در Bash نکته‌ای که باید از همان ابتدا جدی بگیرید Quoting است؛ بسیاری از bugهای معروف Shell از شکستن مقدار روی Space یا گسترش ناخواسته wildcardها ایجاد می‌شوند.

متغیر و Quoting

در Assignment اطراف علامت مساوی Space قرار نمی‌گیرد. مثال صحیح:

name="DJH"
project_dir="$HOME/projects/bash-lab"
printf 'Name: %s\n' "$name"
printf 'Path: %s\n' "$project_dir"

در بیشتر expansionهایی که قرار است یک مقدار واحد باقی بمانند، متغیر را داخل Double Quote قرار دهید؛ یعنی "$variable". فرض کنید مسیر شما Space داشته باشد. این کد خطرناک است:

target="$HOME/My Projects/report.txt"
# نامطمئن:
# rm $target
# مطمئن‌تر:
rm -- "$target"

بدون Quote، Shell ممکن است مقدار را به چند Word تبدیل کند. اضافه کردن -- نیز برای بسیاری از commandها کمک می‌کند نام فایلی که با خط تیره شروع می‌شود به اشتباه Option تفسیر نشود. البته همه commandها از -- پشتیبانی نمی‌کنند، پس این الگو را با شناخت ابزار مقصد استفاده کنید.

Single Quote و Double Quote رفتار یکسانی ندارند. داخل Single Quote، بیشتر expansionها انجام نمی‌شوند:

name="DJH"
printf '%s\n' '$name'
printf '%s\n' "$name"

خط اول رشته $name را به‌صورت literal چاپ می‌کند؛ خط دوم مقدار متغیر را جایگزین می‌کند. این تفاوت در Regex، commandهای پیچیده و ساختن path اهمیت زیادی دارد.

ورودی و Argument

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

read -r -p "Project name: " project_name
printf 'Project: %s\n' "$project_name"

در Argumentها، $1 اولین ورودی، $2 دومین ورودی و $# تعداد Argumentهاست. برای مثال:

#!/usr/bin/env bash
if (( $# < 1 )); then
  printf 'Usage: %s DIRECTORY\n' "$0" >&2
  exit 2
fi
target_dir="$1"
printf 'Target: %s\n' "$target_dir"

برای عبور دادن تمام Argumentها به command یا Function دیگر، تقریباً همیشه "$@" انتخاب امن‌تری است؛ چون مرز بین Argumentها را حفظ می‌کند. استفاده بدون Quote می‌تواند Spaceها و wildcardها را دوباره تفسیر کند.

Command Substitution و Status

برای ذخیره خروجی command داخل متغیر از ساختار $(...) استفاده می‌شود:

current_user="$(whoami)"
file_count="$(find . -maxdepth 1 -type f | wc -l)"
printf 'User: %s | Files: %s\n' "$current_user" "$file_count"

برای محاسبات عددی ساده، Arithmetic Expansion خواناتر است:

processed=7
failed=2
total=$((processed + failed))
printf 'Total: %d\n' "$total"

جدول زیر چند Syntax پرتکرار را برای مراجعه سریع جمع می‌کند. هدف Cheat Sheet کامل نیست؛ فقط مواردی است که در ادامه مقاله مرتب استفاده می‌شوند.

مفهومSyntaxکاربرد
متغیرname="value"ذخیره مقدار
Argument"$1" / "$@"دریافت ورودی command line
خروجی command$(command)گرفتن stdout
محاسبه$((a + b))Arithmetic integer
Exit Status$?وضعیت command قبلی
تعداد Argument$#اعتبارسنجی ورودی

یک قانون عملی خوب این است که هرجا مقدار متنی قرار است «یک Argument» باقی بماند، از خودتان بپرسید آیا باید Quote شود. ShellCheck در همین حوزه بسیار مفید است و بعداً از آن برای پیدا کردن unquoted expansionها و خطاهای مشابه استفاده می‌کنیم.

Environment Variableها نیز نوع خاصی از داده محیطی هستند. متغیری که فقط با name="value" ساخته‌اید در همان Shell موجود است، اما برای اینکه processهای فرزند آن را از Environment دریافت کنند معمولاً باید آن را export کنید. مثلاً بسیاری از ابزارها تنظیماتی مثل مسیر Cache، Token یا mode اجرا را از Environment می‌خوانند. تفاوت Variable داخلی Shell و Environment Variable را از ابتدا بشناسید تا رفتار Script در terminal تعاملی با اجرای خودکار متفاوت نشود.

APP_ENV="test"
export APP_ENV
bash -c 'printf "Child sees: %s\n" "$APP_ENV"'

برای secretها احتیاط بیشتری لازم است. قرار دادن password یا API token به‌صورت hard-coded داخل فایل Script، به‌ویژه اگر repository به Git متصل باشد، ریسک امنیتی ایجاد می‌کند. Environment Variable هم به‌تنهایی سیستم مدیریت Secret نیست و ممکن است در بعضی contextها قابل مشاهده باشد؛ اما دست‌کم جدا کردن Configuration از Source Code طراحی بهتری است. در پروژه جدی از Secret Manager یا سازوکار امن محیط اجرا استفاده کنید و هیچ credential واقعی را در مثال، Log یا trace قرار ندهید.

شرط‌ها و حلقه‌ها

متغیرها Script را قابل تنظیم می‌کنند، اما Condition و Loop آن را هوشمند می‌کنند. با شرط می‌توانید وجود فایل، مقدار Argument یا نتیجه command را بررسی کنید؛ با حلقه می‌توانید همان منطق را روی ده‌ها یا هزاران فایل تکرار کنید. بهتر است مثال‌های Control Flow را با وضعیت واقعی یاد بگیرید، نه فقط مقایسه دو عدد.

شرط و Test

در Bash، ساختار [[ ... ]] برای بسیاری از شرط‌های Bash خوانا و قدرتمند است. مثال زیر مسیر ورودی را بررسی می‌کند:

#!/usr/bin/env bash
target="${1:-}"
if [[ -z "$target" ]]; then
  printf '%s\n' "Path is required" >&2
  exit 2
elif [[ ! -e "$target" ]]; then
  printf 'Not found: %s\n' "$target" >&2
  exit 1
elif [[ -d "$target" ]]; then
  printf 'Directory: %s\n' "$target"
else
  printf 'File: %s\n' "$target"
fi

-z رشته خالی، -e وجود Path و -d Directory بودن را بررسی می‌کند. Bash Conditional Expressionها گزینه‌های بیشتری برای فایل، رشته و عدد دارند، اما لازم نیست همه را حفظ کنید؛ مهم این است که قبل از عملیات destructive ورودی را validate کنید.

گاهی بهترین شرط این است که مستقیماً خود command را اجرا کنید. به‌جای گرفتن $? در خط جدا، می‌توانید بنویسید:

if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
  printf '%s\n' "Git repository detected"
else
  printf '%s\n' "Not a Git repository"
fi

این الگو ساده‌تر است و احتمال خطا ناشی از overwrite شدن $? با command دیگر را کاهش می‌دهد.

Case و چند انتخاب

وقتی یک مقدار چند حالت مشخص دارد، case معمولاً از زنجیره طولانی if/elif خواناتر است. مثال زیر یک mode را می‌گیرد:

mode="${1:-scan}"
case "$mode" in
  scan)
    printf '%s\n' "Scanning..."
    ;;
  backup)
    printf '%s\n' "Backing up..."
    ;;
  clean)
    printf '%s\n' "Cleaning..."
    ;;
  *)
    printf 'Unknown mode: %s\n' "$mode" >&2
    exit 2
    ;;
esac

وجود حالت پیش‌فرض *) مهم است؛ اگر کاربر مقدار اشتباه بدهد، Script نباید بی‌سروصدا به مسیر دیگری برود.

حلقه‌ها

برای پردازش مجموعه‌ای از فایل‌ها، for ساده و خواناست. وقتی از Glob استفاده می‌کنید، دقت کنید expansion قبل از اجرای command انجام می‌شود:

for file in ./*.log; do
  [[ -e "$file" ]] || continue
  printf 'Log: %s\n' "$file"
done

برای خواندن خط‌به‌خط یک فایل، الگوی زیر از بسیاری از خطاهای رایج جلوگیری می‌کند:

while IFS= read -r line; do
  printf 'Line: %s\n' "$line"
done < input.txt

IFS= و -r کمک می‌کنند Spaceهای ابتدا/انتها و Backslashها ناخواسته تغییر نکنند. break حلقه را تمام می‌کند و continue اجرای iteration فعلی را رد می‌کند. در Scriptهای فایل‌محور، این دو برای skip کردن موارد نامعتبر بسیار کاربردی‌اند.

برای تثبیت این بخش، بهتر است قبل از رفتن سراغ Function چند تمرین کوچک انجام دهید. هدف این تمرین‌ها نوشتن برنامه طولانی نیست؛ هرکدام باید فقط یک مفهوم را با ورودی واقعی محک بزنند.

  1. Scriptی بنویسید که یک path را بگیرد و مشخص کند File، Directory یا path ناموجود است؛ برای حالت ناموجود Exit Code غیرصفر بدهید.
  2. یک عدد از Argument بگیرید و با case سه mode مانند fast، safe و test را مدیریت کنید؛ مقدار ناشناخته باید Usage نشان دهد.
  3. فایل‌های .txt یک پوشه آزمایشی را با for فهرست کنید و نام‌های دارای Space را عمداً وارد Test کنید.
  4. یک فایل متنی چندخطی بسازید و آن را با while IFS= read -r بخوانید؛ بررسی کنید Backslash و فاصله ابتدا/انتهای خط چه رفتاری دارند.
  5. یک command عمداً ناموفق اجرا کنید و به‌جای چاپ ساده خطا، بر اساس موفقیت یا شکست آن پیام و Exit Status مناسب تولید کنید.

اگر هر تمرین را بتوانید بدون Copy کردن مثال اصلی از نو بنویسید، Control Flow را به سطح کاربردی رسانده‌اید. اگر هنوز تفاوت شرط روی «خروجی command» و شرط روی «Exit Status command» مبهم است، همان قسمت را دوباره تمرین کنید؛ این تفاوت در Scriptهای واقعی بسیار مهم‌تر از حفظ کردن همه operatorهای شرطی است.

توابع و آرایه‌ها

وقتی Script از چند ده خط عبور می‌کند، تکرار منطق و متغیرهای سراسری مدیریت آن را سخت می‌کند. Functionها کمک می‌کنند کارها را به واحدهای کوچک‌تر تقسیم کنید و Arrayها چند مقدار مستقل را بدون تبدیل خطرناک به یک String نگه می‌دارند. این دو مفهوم برای نوشتن Script قابل نگهداری ضروری‌اند.

Function و Scope

یک Function ساده می‌تواند Validation را از منطق اصلی جدا کند:

require_directory() {
  local path="$1"
  if [[ ! -d "$path" ]]; then
    printf 'Directory not found: %s\n' "$path" >&2
    return 1
  fi
}

target="${1:-.}"
if ! require_directory "$target"; then
  exit 1
fi

local متغیر را به Scope همان Function محدود می‌کند. return وضعیت Function را برمی‌گرداند، در حالی که exit کل Script را خاتمه می‌دهد. در Functionهای reusable معمولاً return انتخاب بهتری است تا caller درباره واکنش تصمیم بگیرد.

برای پاس دادن همه Argumentها به Function دیگر، مرز آن‌ها را حفظ کنید:

run_command() {
  printf 'Running:'
  printf ' %q' "$@"
  printf '\n'
  "$@"
}

run_command printf '%s\n' "file with spaces.txt"

در اینجا "$@" به Bash اجازه می‌دهد هر Argument را جدا نگه دارد. تبدیل مجموعه Argumentها به یک String و اجرای آن با eval معمولاً ریسک quoting و حتی injection را بالا می‌برد و باید فقط در موارد بسیار مشخص و با شناخت کامل استفاده شود.

Array و Argument List

Array برای ساختن optionهای یک command بسیار مناسب است:

args=(--recursive --verbose)
source_dir="$HOME/My Projects/source"
destination="$HOME/Backup"

cp "${args[@]}" -- "$source_dir" "$destination"

اگر به‌جای Array همه optionها را در یک String مثل args="--recursive --verbose" ذخیره کنید، با اضافه شدن مقدارهای دارای Space یا Quote مدیریت آن سخت می‌شود. Array ساختار Argumentها را حفظ می‌کند و باعث می‌شود intent کد واضح‌تر باشد.

برای پیمایش Array:

extensions=(mp3 flac opus wav)
for ext in "${extensions[@]}"; do
  printf 'Extension: %s\n' "$ext"
done

Bash امکانات پیشرفته‌تری مانند Associative Array نیز دارد، اما برای یک مسیر Zero-to-Hero لازم نیست همه جزئیات آن را در همین مقاله باز کنیم. مهم این است که String را جایگزین Array نکنید وقتی واقعاً با «چند Argument مستقل» سروکار دارید.

فایل، Pipe و متن

بخش بزرگی از قدرت Bash از این ایده می‌آید که ابزارهای کوچک را به هم وصل کنید. یک command فایل پیدا می‌کند، command دوم خطوط مورد نیاز را فیلتر می‌کند و command سوم نتیجه را شمارش یا تبدیل می‌کند. این مدل ترکیبی، به‌ویژه برای Log، فایل‌های متنی، Git repository و اتوماسیون Server بسیار مفید است.

فایل و Directory

قبل از عملیات روی فایل‌ها، مسیر جاری و نتیجه expansion را بدانید. فرمان‌های پایه مانند pwd، cd، mkdir، cp، mv، rm و find هسته بسیاری از Scriptها هستند. عملیات حذف را با احتیاط بیشتری بنویسید؛ یک variable خالی یا path اشتباه می‌تواند خسارت ایجاد کند.

مثال زیر فایل‌های .log قدیمی‌تر از هفت روز را فقط فهرست می‌کند و هنوز چیزی حذف نمی‌کند:

target_dir="${1:-.}"
find "$target_dir" -type f -name '*.log' -mtime +7 -print

Quote کردن pattern در -name '*.log' مهم است؛ در غیر این صورت ممکن است Shell قبل از رسیدن pattern به find آن را گسترش دهد. برای عملیات destructive، ابتدا Dry Run داشته باشید و بعد از تأیید logic، action واقعی را اضافه کنید.

Pipe و Redirection

هر process معمولاً سه stream استاندارد دارد: Standard Input، Standard Output و Standard Error. Pipe با علامت | خروجی استاندارد یک command را به ورودی command بعدی می‌دهد. Redirection خروجی را به فایل یا stream دیگری می‌فرستد.

printf '%s\n' "alpha" "error: one" "beta" "error: two" |
  grep 'error:' |
  wc -l

برای نوشتن خروجی در فایل از > و برای append کردن از >> استفاده می‌شود. Standard Error شماره 2 است:

command_that_may_fail >output.log 2>error.log

برای ترکیب stdout و stderr در یک فایل:

command_that_may_fail >run.log 2>&1

ترتیب Redirection مهم است. وقتی Script پیچیده می‌شود، به‌جای حفظ کردن مثال‌ها، مدل streamها را بفهمید: هر command چه ورودی می‌گیرد و خروجی یا خطای آن به کجا می‌رود.

پردازش متن

grep برای انتخاب خطوط، sed برای تبدیل stream متن، awk برای پردازش fieldها و find برای جست‌وجوی فایل‌ها بسیار رایج‌اند. هرکدام ابزار بزرگی هستند و آموزش کامل آن‌ها باید مقاله مستقل داشته باشد، اما چند ترکیب پایه ارزش یادگیری دارد.

مثلاً برای استخراج خط‌های شامل ERROR از Log و نمایش timestamp و message فرضی:

grep 'ERROR' app.log | awk '{print $1, $2, $0}'

یا برای جایگزینی متن در output بدون تغییر فایل اصلی:

printf '%s\n' "Windows Bash" | sed 's/Windows/Cross-platform/'

در پروژه‌های واقعی باید ورودی را بشناسید؛ parsing متن ساختارنیافته با chain طولانی از grep | sed | awk همیشه بهترین معماری نیست. اگر داده JSON، CSV پیچیده یا ساختار برنامه‌ای دارد، ابزار تخصصی مانند jq یا حتی Python می‌تواند قابل اعتمادتر باشد. Bash برای orchestration عالی است، اما قرار نیست جای همه زبان‌ها را بگیرد.

در عملیات فایل یک اصل مهم دیگر وجود دارد: بین «مشاهده»، «برنامه‌ریزی تغییر» و «اعمال تغییر» فاصله بگذارید. به‌عنوان مثال ابتدا با find ... -print مجموعه هدف را ببینید؛ سپس در Dry Run دستور نهایی را فقط چاپ کنید؛ در مرحله آخر Copy، Move یا Delete واقعی را فعال کنید. این الگو به‌خصوص روی Windows مهم است، چون ممکن است Script از WSL به mount یک درایو بزرگ دسترسی داشته باشد و یک path اشتباه دامنه عملیات را بسیار وسیع کند.

برای حذف گروهی فایل‌ها، به جای ساخت command با Stringهای ترکیبی، targetها را به‌شکل ساختاریافته عبور دهید و قبل از action شروط محافظ داشته باشید. هیچ‌وقت متغیر خالی را کنار rm -rf یا command مشابه بی‌محابا قرار ندهید. اگر داده ارزشمند است، Backup و recoverability بخشی از طراحی Script است، نه کاری که بعد از حادثه انجام می‌شود. اتوماسیون خوب تعداد کلیک‌ها را کم می‌کند، اما باید احتمال خطای انسانی را هم کاهش دهد.

تفاوت‌های Windows و Linux

اگر فقط Syntax Bash را یاد بگیرید اما تفاوت محیط Windows و Linux را نشناسید، دیر یا زود با Scriptی روبه‌رو می‌شوید که «روی یک سیستم کار می‌کند و روی دیگری نه». مهم‌ترین علت‌ها معمولاً Path، Line Ending، Permission، محل فایل و availability ابزارها هستند. این بخش همان جایی است که آموزش Bash روی Windows با یک آموزش عمومی Bash تفاوت جدی پیدا می‌کند.

مسیرها در WSL و Git Bash

مسیر بومی Windows مثل C:\Users\DJH\Projects در WSL معمولاً زیر mount درایو C دیده می‌شود: /mnt/c/Users/DJH/Projects. Git Bash الگوی متفاوتی دارد و معمولاً همان درایو را به شکل /c/Users/DJH/Projects می‌بیند. بنابراین یک path hard-coded که در Git Bash کار می‌کند الزاماً در WSL همان شکل را ندارد.

مقایسه مسیرها در WSL و Git Bash با Windows برای آموزش Bash Script ویندوز، تبدیل مسیر درایوها و مدیریت صحیح Space در فایل‌ها
ساختار مسیر در WSL و Git Bash متفاوت است؛ شناخت mount درایوهای Windows و استفاده از کوتیشن برای مسیرهای دارای Space ضروری است.

جدول زیر سه نمایش رایج یک مسیر را مقایسه می‌کند:

WindowsWSLGit Bash
C:\Users\DJH/mnt/c/Users/DJH/c/Users/DJH
D:\Projects/mnt/d/Projects/d/Projects
C:\My Files\A/mnt/c/My Files/A/c/My Files/A

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

target="/mnt/c/My Files/report.txt"
printf '%s\n' "$target"
# همیشه expansion مسیر را در این سناریو quote کنید:
cat -- "$target"

اگر Script باید در چند محیط اجرا شود، تا حد امکان pathهای absolute مخصوص یک ماشین را از منطق اصلی جدا کنید. Path را Argument، Environment Variable یا Configuration کنید تا Script مجبور نباشد نام کاربر یا درایو را حدس بزند.

فایل‌سیستم WSL

WSL هم به فایل‌سیستم Linux خودش دسترسی دارد و هم درایوهای Windows را mount می‌کند. اگر workflow شما عمدتاً Linux-based است، Microsoft توصیه می‌کند فایل‌های پروژه را برای بهترین Performance در فایل‌سیستم WSL نگه دارید؛ مثلاً زیر ~/projects. اگر ابزارهای اصلی شما Windows-native هستند، نگهداری فایل در فایل‌سیستم Windows طبیعی‌تر است.

این تصمیم فقط Performance نیست. Permissionها، case sensitivity، metadata و رفتار بعضی ابزارها بین فایل‌سیستم Linux و NTFS mount شده تفاوت دارد. بنابراین اگر یک پروژه در WSL ساخته شده و قرار است toolchain Linux آن را build کند، نگهداری آن در home WSL معمولاً workflow تمیزتری می‌سازد. در مقابل، فایلی که باید مرتب با نرم‌افزارهای Windows ویرایش شود ممکن است بهتر باشد روی Windows بماند.

برای دسترسی از Windows به فایل‌های WSL، از مسیرهای ارائه‌شده توسط WSL/File Explorer استفاده کنید و از دستکاری مستقیم فایل‌های داخلی توزیع از مسیرهای سیستمی ناشناخته خودداری کنید. هدف این است که مرز filesystemها را آگاهانه مدیریت کنید، نه اینکه یک پروژه را دائماً بین دو semantics متفاوت جابه‌جا کنید.

LF و CRLF

Windows به‌طور سنتی CRLF و Unix/Linux از LF برای Line Ending استفاده می‌کنند. بسیاری از Editorهای مدرن این موضوع را پنهان می‌کنند، اما Shell Script نسبت به وجود Carriage Return اضافی در جاهای حساس می‌تواند واکنش بدی نشان دهد. یکی از علامت‌های کلاسیک، مشاهده ^M یا خطای bad interpreter در Shebang است.

برای repositoryهای چندسکویی، Git اجازه می‌دهد Line Ending را با .gitattributes کنترل کنید. یک تنظیم ساده و شفاف برای فایل‌های Shell:

*.sh text eol=lf

اگر repository موجود قبلاً Line Endingهای نامنظم دارد، قبل از تغییر گسترده وضعیت را بررسی کنید؛ normalization می‌تواند تعداد زیادی فایل را modified نشان دهد. هدف این است که policy پروژه مشخص باشد، نه اینکه تنظیمات Git را بدون درک اثر آن تغییر دهید.

در VS Code و بسیاری از Editorها می‌توانید LF/CRLF فایل جاری را ببینید و تغییر دهید. برای Bash Scriptهایی که قرار است در Linux، WSL یا CI اجرا شوند، LF انتخاب قابل پیش‌بینی‌تری است.

Permission و Portability

Permission در WSL روی فایل‌سیستم Linux رفتاری نزدیک به Linux دارد، اما روی فایل‌های mount شده از Windows جزئیات بیشتری وارد ماجرا می‌شود. به همین دلیل نباید از یک مثال ساده chmod +x نتیجه بگیرید که Permission روی همه pathهای WSL و Git Bash دقیقاً مانند ext4 رفتار می‌کند.

برای Portability، وابستگی‌های Script را نیز کنترل کنید. صرف اینکه Bash موجود است به معنی وجود همه commandها نیست. قبل از استفاده از ابزار ضروری می‌توانید آن را بررسی کنید:

require_command() {
  local cmd="$1"
  if ! command -v "$cmd" >/dev/null 2>&1; then
    printf 'Required command not found: %s\n' "$cmd" >&2
    return 1
  fi
}

require_command git || exit 1
require_command grep || exit 1

اگر هدف cross-platform است، Bash-specific featureها را آگاهانه انتخاب کنید. Scriptی که با #!/usr/bin/env bash نوشته شده می‌تواند از امکانات Bash استفاده کند، اما نباید آن را بدون بررسی به‌عنوان POSIX sh معرفی کنید. برعکس، اگر واقعاً portability بین Shellهای POSIX هدف است، Syntax را محدودتر نگه دارید و با Shell مناسب تست کنید.

برای Portability بهتر است assumptionهای محیط را به یک Checklist صریح تبدیل کنید. مشخص کنید Script به Bash نیاز دارد یا با sh هم سازگار است، چه commandهای خارجی لازم دارد، آیا path خاصی hard-code شده، آیا فایل ورودی باید LF باشد و آیا به Permission executable وابسته است. سپس حداقل یک‌بار Script را در Environment مقصد واقعی اجرا کنید؛ تست موفق در Git Bash به‌تنهایی ثابت نمی‌کند که رفتار روی Ubuntu Server، WSL یا runner لینوکسی یکسان خواهد بود.

  • Interpreter هدف و Shebang را مشخص کنید و آن را با Syntax واقعی Script هماهنگ نگه دارید.
  • Pathهای مخصوص کاربر یا درایو را تا حد ممکن به Argument یا Configuration منتقل کنید.
  • نام فایل دارای Space، Tab یا کاراکترهای غیرمعمول را در تست‌ها فراموش نکنید.
  • وجود commandهای خارجی مانند git، jq، rsync یا ffmpeg را قبل از استفاده بررسی کنید.
  • برای فایل‌های .sh Line Ending قابل پیش‌بینی تعریف کنید و repository را روی سیستم دوم هم checkout و تست کنید.
  • اگر behavior به GNU-specific option وابسته است، آن را در Requirements بنویسید و از ادعای POSIX portability خودداری کنید.

این Checklist خصوصاً زمانی ارزش دارد که Script قرار است از لپ‌تاپ شخصی به Server، CI runner یا سیستم همکار منتقل شود. Portability به معنی «هیچ تفاوتی بین سیستم‌ها وجود ندارد» نیست؛ یعنی تفاوت‌ها را شناخته‌اید، dependencyها را آشکار کرده‌اید و Script در محدوده‌ای که وعده داده‌اید تست شده است.

دیباگ و کدنویسی مطمئن

نوشتن Script فقط مرحله اول است؛ مرحله مهم‌تر این است که بتوانید قبل از اجرای واقعی خطاها را پیدا کنید و هنگام شکست علت را بفهمید. Bash ابزارهای داخلی برای Syntax Check و Trace دارد و ShellCheck نیز لایه Static Analysis اضافه می‌کند. ترکیب این ابزارها بسیار مؤثرتر از این است که Script را بارها اجرا کنید و فقط به خروجی نگاه کنید.

Syntax و Trace

برای بررسی Syntax بدون اجرای معمول commandهای Script از bash -n استفاده کنید:

bash -n script.sh

اگر command خروجی ندهد و Exit Status صفر باشد، Bash خطای Syntax آشکاری پیدا نکرده است؛ البته این به معنی درست بودن منطق Script نیست. برای دیدن commandهایی که هنگام اجرا expansion شده‌اند، bash -x بسیار مفید است:

bash -x script.sh arg1 arg2

Trace می‌تواند اطلاعات حساس مثل token یا password را نمایش دهد، بنابراین در Scriptهایی که secret دارند با احتیاط استفاده کنید و Log آن را منتشر نکنید. می‌توانید tracing را فقط در بخش مورد نیاز فعال و غیرفعال کنید:

set -x
important_command "$@"
set +x

اما اگر Argumentها حساس هستند حتی همین روش هم مناسب نیست. Debugging خوب یعنی ابزار را متناسب با سطح حساسیت اجرا کنید.

ShellCheck

ShellCheck یک Static Analysis Tool برای sh و bash است که مشکلات رایج Syntax، Quoting، Condition، Array، Command usage و edge caseهای رفتاری را گزارش می‌کند. ارزش اصلی آن این است که برخی ایرادها ممکن است روی فایل آزمایشی شما دیده نشوند اما بعداً با path دارای Space، wildcard یا ورودی متفاوت ظاهر شوند.

اگر ShellCheck در محیط شما نصب است، اجرای پایه بسیار ساده است:

shellcheck script.sh

نمونه‌ای از کدی که ظاهراً ساده است اما می‌تواند مشکل‌ساز شود:

file="$HOME/My Files/report.txt"
# بد:
# cat $file
# بهتر:
cat "$file"

ShellCheck جای تست واقعی را نمی‌گیرد. Linter نمی‌داند Server مقصد شما چه فایل‌هایی دارد یا Business Logic صحیح چیست. Workflow مناسب این است: Syntax Check، ShellCheck، Test با داده کنترل‌شده و در نهایت اجرای واقعی با محافظ‌های لازم.

Error Handling امن

در اینترنت زیاد با توصیه set -euo pipefail به‌عنوان «Strict Mode» روبه‌رو می‌شوید. این گزینه‌ها مفیدند، اما جادویی نیستند و به‌خصوص set -e context-dependent است؛ در شرط‌ها، pipelineها و ساختارهای مختلف رفتارهایی دارد که باید شناخته شود. برای مبتدی بهتر است به‌جای تکیه مطلق بر یک خط تنظیمات، failureهای مهم را صریح مدیریت کند.

مثلاً اگر Backup فقط در صورت موفقیت archive باید ادامه پیدا کند:

if ! tar -czf "$archive" "$source_dir"; then
  printf 'Backup failed: %s\n' "$source_dir" >&2
  exit 1
fi
printf 'Backup created: %s\n' "$archive"

همچنین قبل از کار destructive، ورودی خالی، وجود مسیر و نوع آن را بررسی کنید. برای commandهای حساس Dry Run بسازید. Log را طوری بنویسید که نام عملیات، target و نتیجه مشخص باشد. اگر قرار است Script از Task Scheduler یا CI اجرا شود، Exit Code صحیح از پیام رنگی یا متن زیبا مهم‌تر است.

Logging نیز باید قابل بازتولید باشد. حداقل مشخص کنید عملیات روی چه targetی انجام شده، چه زمانی شروع یا تمام شده و نتیجه success، skip یا failure بوده است. اگر Script چند فایل را پردازش می‌کند، Summary نهایی مثل تعداد processed و failed کمک می‌کند بدون خواندن صدها خط Log وضعیت اجرا را بفهمید. از طرف دیگر Secret، token، password و داده حساس را داخل Log چاپ نکنید؛ Trace مفید نباید به نشت اطلاعات تبدیل شود.

برای Debug یک نسخه کوچک از مسئله بسازید. اگر Script روی ده‌هزار فایل fail می‌شود، ابتدا همان خطا را روی سه فایل reproduce کنید. اگر path مشکل دارد، مقدار آن را با delimiter قابل مشاهده چاپ کنید تا Space انتهایی یا مقدار خالی پنهان نماند. اگر command external شکست می‌خورد، همان command نهایی را جداگانه اجرا کنید. این رویکرد مرحله‌ای سریع‌تر از اضافه کردن echoهای تصادفی در همه جای Script است و بعداً حذف کدهای Debug را نیز ساده‌تر می‌کند.

  • قبل از اجرای واقعی، bash -n را اجرا کنید.
  • ShellCheck را روی فایل اجرا کنید و Warningها را بفهمید؛ فقط آن‌ها را disable نکنید.
  • Path و variableهای متنی را درست Quote کنید.
  • ابزارهای لازم را با command -v بررسی کنید.
  • برای حذف، انتقال انبوه یا overwrite، Dry Run و Validation داشته باشید.
  • خطا را روی stderr بنویسید و Exit Code غیرصفر برگردانید.
  • Script را با path دارای Space و نام‌های غیرمعمول تست کنید.

خطاهای رایج در ویندوز

برای عیب‌یابی Bash روی Windows، از خود Error Message شروع کنید اما همان‌جا متوقف نشوید.

خطاهای رایج Bash در ویندوز شامل bad interpreter، Permission denied و command not found همراه با علت‌ها و روش‌های عیب‌یابی WSL و Git Bash
راهنمای عیب‌یابی Bash در Windows برای شناسایی مشکلات Line Ending، Shebang، Permission، PATH و تفاوت محیط‌های WSL و Git Bash در اجرای اسکریپت‌ها

خطا معمولاً نشانه یک دسته مشکل است: Interpreter، Line Ending، Permission، Path یا Dependency. جدول زیر چند مورد پرتکرار را به علت احتمالی و مسیر حل وصل می‌کند.

خطا یا علامتعلت محتملراه‌حل اولیه
bad interpreter یا ^MCRLF یا Shebang نامعتبرفایل را LF کنید و Shebang را بررسی کنید
Permission deniedمجوز اجرا یا رفتار filesystemروش اجرا و Permission/محل فایل را بررسی کنید
command not foundابزار نصب نیست یا در PATH نیستcommand -v و Environment را بررسی کنید
No such file or directoryPath اشتباه، Quote ناقص یا interpreter غایبpwd، ls و path واقعی را بررسی کنید
کار در WSL و شکست در Git Bashوابستگی Linux-specific یا تفاوت Path/tooldependency و environment assumptions را مشخص کنید

خطاهای Shebang و CRLF

اگر Script با bash script.sh اجرا می‌شود اما با ./script.sh خطای interpreter می‌دهد، Shebang و Line Ending را بررسی کنید. وجود CR در انتهای خط اول ممکن است باعث شود سیستم interpreter را چیزی شبیه /usr/bin/env bash\r تصور کند. Editor را روی LF قرار دهید و در repository برای *.sh policy مشخص تعریف کنید.

برای دیدن characterهای غیرعادی می‌توانید از ابزارهایی مانند cat -v در محیط مناسب استفاده کنید:

cat -v script.sh | head

اگر ^M دیده می‌شود، قبل از هر تغییر دیگری Line Ending را اصلاح کنید. تبدیل فایل را با Editor مطمئن انجام دهید تا Encoding و محتوای Script ناخواسته خراب نشود.

Permission و Command not found

در WSL روی فایل‌سیستم Linux، اگر اجرای مستقیم فایل مجاز نیست، وضعیت آن را بررسی کنید:

ls -l script.sh
chmod +x script.sh

اما اگر فایل روی /mnt/c است یا در Git Bash اجرا می‌شود، فرض نکنید تمام semantics دقیقاً همان Linux native است. یک تست ساده این است که ابتدا bash script.sh را امتحان کنید؛ اگر این روش موفق است، محتوا و Syntax احتمالاً سالم‌اند و مشکل به شیوه اجرای مستقیم، Shebang یا metadata نزدیک‌تر است.

برای command not found ابتدا مشخص کنید command باید built-in Bash باشد یا برنامه خارجی:

command -v git
command -v awk
command -v shellcheck

اگر خروجی خالی است، ممکن است ابزار نصب نباشد یا PATH آن را پوشش ندهد. نصب Package نیز بین WSL و Git Bash یکسان نیست؛ در WSL از package manager توزیع استفاده می‌کنید، اما Git Bash یک Linux distribution عمومی با apt نیست.

Path و Dependency

خطای No such file or directory همیشه به معنی نبود خود فایل نیست. ممکن است path با Space بدون Quote شکسته باشد، Case اشتباه باشد، Shebang به interpreter ناموجود اشاره کند یا Script path مخصوص Git Bash را داخل WSL استفاده کند. برای تشخیص مرحله‌ای این چند فرمان کمک می‌کند:

pwd
printf 'Target: <%s>\n' "$target"
ls -ld -- "$target"
command -v bash

اگر Script قرار است روی چند سیستم اجرا شود، Environment assumptionها را مستند کنید: Bash لازم است؟ Git لازم است؟ rsync یا jq لازم است؟ مسیر پیش‌فرض کجاست؟ هر dependency پنهان، یک خطای آینده است. مقاله پیشنهادی آینده «رفع خطاهای Bash Script در ویندوز» می‌تواند این شاخه‌ها را با مثال‌های بیشتر جداگانه گسترش دهد.

پروژه عملی Bash

برای جمع‌بندی مهارت‌ها، یک سناریوی عملی می‌سازیم: Scriptی که فایل‌های یک پوشه را بر اساس extension فهرست می‌کند، مقصد را می‌سازد، Dry Run دارد، در حالت واقعی فایل‌ها را کپی می‌کند و Log قابل خواندن تولید می‌کند. این یک مثال آموزشی است، نه تجربه شخصی یا ابزار Production آماده؛ هدف آن نشان دادن Architecture مناسب Script است.

طراحی پروژه

قبل از نوشتن کد، Requirement را دقیق کنیم. Script باید Source Directory را از Argument بگیرد، Destination را با Option مشخص کند، حالت --dry-run داشته باشد و فقط چند extension نمونه را پردازش کند. فایل‌های موجود در مقصد نباید بی‌صدا overwrite شوند. همچنین هر خطا باید Exit Code مناسب داشته باشد.

طراحی پروژه Bash Script برای ویندوز شامل اعتبارسنجی مسیرها، پردازش آرگومان‌ها، Dry Run، مدیریت فایل‌ها، کنترل خطا و تعیین Exit Code مناسب
ساختار اصولی Bash Script با تعریف Requirement، بررسی Dependency، مدیریت Optionها، اجرای امن Copy و گزارش خطا، پایه‌ای مطمئن برای اتوماسیون ویندوز فراهم می‌کند.
  1. اعتبارسنجی Source و Destination.
  2. بررسی وجود commandهای لازم.
  3. خواندن Optionهای command line.
  4. ساخت مقصد فقط در حالت واقعی.
  5. پیدا کردن فایل‌های مجاز.
  6. نمایش Action در Dry Run و اجرای Copy در حالت واقعی.
  7. ثبت شمارنده موفق/خطا و Exit Code نهایی.

این طراحی عمداً محدود است. اگر نیاز شما sync پیچیده، checksum، resume یا backup incremental است، ابزار تخصصی مانند rsync یا نرم‌افزار Backup ممکن است مناسب‌تر باشد. Bash باید orchestration را ساده کند، نه اینکه ابزارهای بالغ را از صفر بازنویسی کند.

ساخت مرحله‌ای Script

نسخه آموزشی زیر concepts اصلی مقاله را کنار هم قرار می‌دهد:

#!/usr/bin/env bash

usage() {
  printf 'Usage: %s SOURCE DEST [--dry-run]\n' "$0"
}

log() {
  printf '[%s] %s\n' "$(date '+%H:%M:%S')" "$*"
}

source_dir="${1:-}"
dest_dir="${2:-}"
mode="${3:-}"

if [[ -z "$source_dir" || -z "$dest_dir" ]]; then
  usage >&2
  exit 2
fi

if [[ ! -d "$source_dir" ]]; then
  printf 'Source directory not found: %s\n' "$source_dir" >&2
  exit 1
fi

dry_run=false
case "$mode" in
  "") ;;
  --dry-run) dry_run=true ;;
  *)
    printf 'Unknown option: %s\n' "$mode" >&2
    usage >&2
    exit 2
    ;;
esac

if [[ "$dry_run" == false ]]; then
  mkdir -p -- "$dest_dir" || exit 1
fi

processed=0
failed=0
extensions=(mp3 flac opus wav)

for ext in "${extensions[@]}"; do
  while IFS= read -r -d '' file; do
    name="$(basename -- "$file")"
    target="$dest_dir/$name"

    if [[ -e "$target" ]]; then
      log "SKIP exists: $target"
      continue
    fi

    if [[ "$dry_run" == true ]]; then
      log "DRY-RUN copy: $file -> $target"
      ((processed += 1))
      continue
    fi

    if cp -- "$file" "$target"; then
      log "COPIED: $file -> $target"
      ((processed += 1))
    else
      log "ERROR: $file"
      ((failed += 1))
    fi
  done < <(find "$source_dir" -maxdepth 1 -type f -name "*.$ext" -print0)
done

log "Processed: $processed | Failed: $failed"

if (( failed > 0 )); then
  exit 1
fi
exit 0

این Script از Array برای extensionها، Function برای Log/Usage، شرط برای validation، Process Substitution برای feeding خروجی find و null delimiter برای نام فایل‌های دارای Space استفاده می‌کند. وجود -print0 و read -d '' باعث می‌شود نام فایل با Space شکسته نشود. این Syntax Bash-specific است و به همین دلیل Shebang نیز Bash را هدف قرار می‌دهد.

برای تست Dry Run:

bash file-copy.sh "/path/to/source" "/path/to/dest" --dry-run

بعد از بررسی خروجی، حالت واقعی را اجرا کنید:

bash file-copy.sh "/path/to/source" "/path/to/dest"

در محیط WSL اگر Source روی Windows باشد path را به شکل /mnt/c/... بدهید. در Git Bash شکل معمول آن /c/... است. همین یک مثال نشان می‌دهد چرا Configuration و Argument بهتر از hard-code کردن path داخل Script است.

تست و Portability

قبل از اینکه Script را روی داده مهم اجرا کنید، یک پوشه Test کوچک بسازید. فایل‌هایی با Space، extensionهای مختلف و یک فایل هم‌نام در Destination ایجاد کنید و ببینید Script چه رفتاری دارد. سپس Syntax و Static Analysis را اجرا کنید:

bash -n file-copy.sh
shellcheck file-copy.sh

اگر ShellCheck Warning می‌دهد، هر Warning را بررسی کنید. بعضی warningها ممکن است به‌دلیل تصمیم آگاهانه شما باشند، اما disable کردن کورکورانه آن‌ها ارزش Linter را از بین می‌برد. بعد Script را در محیط دوم نیز Test کنید؛ مثلاً اگر ابتدا در WSL نوشته شده، رفتار آن را در Git Bash بررسی کنید و dependencyها را یادداشت کنید.

چند تمرین برای گسترش پروژه:

  • Option به نام --move اضافه کنید که به‌جای Copy از Move استفاده کند.
  • فیلتر extensionها را از Argument بگیرید.
  • یک فایل Log اختیاری اضافه کنید و stderr را جدا نگه دارید.
  • قبل از Copy اندازه فایل را بررسی کنید.
  • قابلیت recursive scan را با Option جدا اضافه کنید.
  • یک --help استاندارد و readable بسازید.

اگر قصد دارید چنین اسکریپت‌هایی را برای workflow واقعی و حساس توسعه دهید، طراحی Safety، Logging، rollback و test اهمیت بیشتری پیدا می‌کند. برای مشاهده نمونه‌ای از رویکرد اتوماسیون فایل در اکوسیستم DJH می‌توانید اسکریپت پاکسازی و استانداردسازی نام فایل‌ها را ببینید؛ آن صفحه PowerShell است و قرار نیست کدش به Bash منتقل شود، اما نوع مسئله اتوماسیون و اهمیت Dry Run و کنترل عملیات را نشان می‌دهد.

Bash یا PowerShell؟

اگر روی Windows هستید، سؤال نهایی معمولاً این نیست که «Bash بهتر است یا PowerShell؟»؛ سؤال درست این است که workload شما به کدام ecosystem نزدیک‌تر است. Bash در orchestration ابزارهای Unix/Linux، pipelineهای متنی، Server، SSH، Git و workflowهای چندسکویی بسیار طبیعی است. PowerShell در مدیریت Windows و اکوسیستم Microsoft، کار با Objectها و APIهای مدیریتی ویندوز مزیت جدی دارد.

مقایسه Bash و PowerShell در ویندوز برای مدیریت سرور، DevOps، SSH، Pipeline، Windows Administration و انتخاب ابزار مناسب برای اتوماسیون و اسکریپت‌نویسی
Bash برای workflowهای Linux، Git و CI/CD مناسب است؛ PowerShell در مدیریت Windows، Active Directory و اکوسیستم Microsoft توانایی بیشتری ارائه می‌دهد.

برای مقایسه زمینه‌ای PowerShell با Shellهای سنتی Windows می‌توانید بررسی PowerShell و Batch ویندوز را نیز بخوانید. جدول زیر تصمیم را در محدوده همین مقاله خلاصه می‌کند.

معیارBashPowerShell
Linux / Server / SSHانتخاب طبیعی و گستردهقابل استفاده، اما معمولاً ecosystem اصلی نیست
Windows Administrationمناسب برخی taskها، به‌خصوص از WSL/Git toolsانتخاب بومی‌تر برای سرویس‌ها، Registry و Microsoft ecosystem
Pipelineعمدتاً stream متن و processهاPipeline مبتنی بر Objectهای .NET
Cross-platform DevOpsبسیار رایج در Linux-oriented toolingPowerShell 7 چندسکویی است و در برخی محیط‌ها مناسب‌تر است
یادگیری روی Windowsنیازمند فهم WSL/Git Bash و تفاوت filesystemشروع مستقیم‌تر برای Windows-native tasks

اگر هر روز با Server Linux، Git، container، SSH یا CI/CD سروکار دارید، Bash را جدی یاد بگیرید. اگر وظیفه اصلی شما مدیریت Windows endpoint، Windows Server، Active Directory، Registry، Microsoft 365 یا APIهای مدیریتی Windows است، PowerShell احتمالاً ابزار مرکزی شما خواهد بود. در بسیاری از تیم‌ها پاسخ واقعی «هر دو» است؛ Bash برای بخش Linux-oriented و PowerShell برای Windows-oriented workflow.

انتخاب بر اساس کار

برای تصمیم سریع، به محل اجرای نهایی نگاه کنید. اگر Script قرار است روی Linux Server یا runner لینوکسی اجرا شود، Bash انتخاب منطقی‌تری است. اگر Script فقط برای مدیریت Computerهای Windows نوشته می‌شود و داده ساختاریافته زیادی از cmdletها می‌گیرد، PowerShell معمولاً کد قابل نگهداری‌تری می‌دهد. اگر فقط چند command Git و Unix-like روی Windows می‌خواهید، Git Bash کافی است و لازم نیست WSL را صرفاً برای داشتن prompt متفاوت نصب کنید.

همچنین لازم نیست همه بخش‌های یک workflow را با یک زبان بنویسید. PowerShell می‌تواند wsl.exe را فراخوانی کند و WSL نیز می‌تواند executableهای Windows را در سناریوهای پشتیبانی‌شده اجرا کند. Integration مفید است، اما مرز environment را واضح نگه دارید تا quoting، path و Exit Code مبهم نشوند.

مسیر بعد از یادگیری Bash

اگر مباحث این مقاله را تمرین کرده باشید، مرحله بعدی حفظ کردن commandهای بیشتر نیست؛ باید یک مسیر تخصصی انتخاب کنید. برای DevOps، Linux fundamentals، Git، SSH، Docker و CI/CD اهمیت پیدا می‌کنند. برای Script quality، ShellCheck و testing را عمیق‌تر کنید. برای Windows automation، مسیر PowerShell از صفر تا صد مکمل مستقیم این آموزش است.

مسیر یادگیری Bash پس از مباحث پایه شامل مدیریت سیستم، پردازش متن، کیفیت اسکریپت، DevOps، لینوکس و اتوماسیون ویندوز با PowerShell
نقشه راه آموزش Bash مسیر پیشرفت از دستورات پایه و Shell Scripting تا تست اسکریپت، Linux، DevOps و ترکیب Bash با PowerShell را نشان می‌دهد.

می‌توانید مسیر مهارتی خود را به‌صورت یک Skill Graph ساده در نظر بگیرید. همه شاخه‌ها اجباری نیستند؛ هر شاخه بر اساس کاری که واقعاً انجام می‌دهید انتخاب می‌شود:

  1. پایه: Terminal، Bash syntax، Quoting، Variable، Condition، Loop و Function.
  2. کار با سیستم: File/Directory، Permission، Environment Variable، Process و Exit Status.
  3. پردازش داده متنی: Pipe، Redirection، grep، sed، awk، find و در صورت نیاز ابزارهای ساختاریافته‌تری مانند jq.
  4. کیفیت Script: bash -n، bash -x، ShellCheck، Dry Run، Logging، Validation و Test با ورودی‌های مرزی.
  5. مسیر Linux/DevOps: Linux fundamentals، SSH، Git، Docker، CI/CD و automation روی Server.
  6. مسیر Windows: PowerShell، Task Scheduler، Windows services و ابزارهای مدیریتی Microsoft.

اگر هدف شما DevOps است، بعد از سطح چهارم سریع‌تر وارد Linux، Git و CI/CD شوید. اگر هدف Automation شخصی روی Windows است، احتمالاً ترکیب Bash برای ابزارهای cross-platform و PowerShell برای Windows-native taskها بیشترین بازده را دارد. اگر هدف توسعه Scriptهای قابل توزیع است، Testing روی بیش از یک Environment و مستندسازی dependencyها را به مهارت اصلی تبدیل کنید.

سوالات متداول

در این بخش به پرسش‌های کوتاه و پرتکرار درباره اجرای Bash Script در ویندوز، انتخاب محیط، خطاهای رایج و سازگاری Scriptها پاسخ می‌دهیم.

آیا Bash Script روی ویندوز اجرا می‌شود؟

بله. دو روش رایج WSL و Git Bash هستند. WSL یک محیط GNU/Linux در Windows فراهم می‌کند و Git Bash یک محیط Bash سازگار با Windows همراه Git for Windows است.

برای Bash در ویندوز WSL بهتر است یا Git Bash؟

برای یادگیری عمیق Bash، ابزارهای Linux، Server و DevOps معمولاً WSL انتخاب کامل‌تری است. برای Git و Scriptهای سبک روی فایل‌های Windows، Git Bash ساده‌تر و سریع‌تر راه‌اندازی می‌شود.

چگونه فایل .sh را در Windows 11 اجرا کنیم؟

فایل را در WSL یا Git Bash باز کنید و با bash script.sh اجرا کنید. در محیطی که Permission اجرایی معنا دارد، پس از تنظیم دسترسی می‌توانید از ./script.sh نیز استفاده کنید.

تفاوت مسیر C:\ در WSL و Git Bash چیست؟

در WSL درایو C معمولاً زیر /mnt/c در دسترس است، در حالی که Git Bash معمولاً همان درایو را با ساختاری مانند /c نمایش می‌دهد. Hard-code کردن Path مخصوص یک محیط، قابلیت حمل Script را کاهش می‌دهد.

خطای /bin/bash^M: bad interpreter چیست؟

این خطا معمولاً به Line Ending از نوع CRLF مربوط است، در حالی که Script باید با LF ذخیره شود. Line Ending را در Editor به LF تبدیل کنید و برای فایل‌های .sh سیاست مناسب .gitattributes در نظر بگیرید.

چرا Bash Script خطای Permission denied می‌دهد؟

ممکن است فایل executable نباشد، روی File System با رفتار متفاوت قرار گرفته باشد یا دسترسی لازم به فایل یا مقصد را نداشته باشید. ابتدا اجرای مستقیم با bash script.sh، سپس Permission و محل فایل را بررسی کنید.

آیا پسوند .sh برای Bash Script اجباری است؟

خیر. Bash می‌تواند Script متنی را بدون پسوند .sh نیز اجرا کند. این پسوند یک Convention مفید برای شناسایی فایل، Editor و ابزارهای توسعه است.

آیا Script نوشته‌شده در Git Bash روی Linux هم اجرا می‌شود؟

ممکن است، اما تضمینی نیست. Script نباید به Pathهای مخصوص Git Bash، Executableهای Windows یا رفتار اختصاصی MSYS2 وابسته باشد و بهتر است روی Linux یا WSL نیز تست و با ShellCheck بررسی شود.

Bash برای اتوماسیون ویندوز بهتر است یا PowerShell؟

برای Workflowهای Linux-oriented و چندسکویی، Bash بسیار مناسب است؛ برای مدیریت بومی Windows معمولاً PowerShell انتخاب مستقیم‌تری است. نوع Task و محیط اجرای نهایی باید تصمیم را تعیین کند.

ShellCheck چیست و چرا مفید است؟

ShellCheck یک ابزار Static Analysis برای Shell Script است که بسیاری از مشکلات Quoting، Condition، Argument، Array و استفاده از Commandها را قبل از اجرای واقعی شناسایی می‌کند. استفاده از آن مکمل تست است، نه جایگزین تست.

جمع بندی داریوش

Bash روی Windows کاملاً قابل استفاده است، اما کیفیت تجربه شما بیش از هر چیز به انتخاب Environment مناسب و شناخت مرز میان Windows و Linux وابسته است. WSL زمانی ارزش بیشتری دارد که به محیط واقعی‌تر Linux، ابزارهای Server و مسیر DevOps نزدیک هستید؛ Git Bash نیز برای Git، Commandهای Unix-like و Scriptهای سبک روی Windows انتخاب ساده و کاربردی‌تری است.

آموزش Bash Script در ویندوز با WSL و Git Bash؛ از نصب و اولین اسکریپت تا متغیر، حلقه، Pipe، ShellCheck، رفع خطا و پروژه عملی و ...
آموزش Bash Script در ویندوز با WSL و Git Bash؛ از نصب و اولین اسکریپت تا متغیر، حلقه، Pipe، ShellCheck، رفع خطا و پروژه عملی و ...

پس از انتخاب محیط، مهم‌ترین بخش یادگیری Bash حفظ کردن تعداد زیادی Command نیست. Variable، Quoting، Argument، Condition، Loop، Function، Pipe و Redirection پایه‌های اصلی هستند؛ اما برای Script قابل اعتماد باید Path، Exit Code، LF و CRLF، Permission، Validation و Debugging را هم از ابتدا جدی بگیرید. استفاده از bash -n، bash -x و ShellCheck کمک می‌کند خطاها قبل از اجرای Script روی داده واقعی پیدا شوند.

بهترین مسیر ادامه این است که همین مفاهیم را روی پروژه‌های کوچک و داده غیرحساس تمرین کنید، سپس Scriptهای خود را با Argument، Dry Run، Logging و Error Handling توسعه دهید. اگر مقصد نهایی شما Linux، Server، Git یا CI/CD است Bash مهارتی بنیادی خواهد بود؛ اگر تمرکز اصلی روی مدیریت بومی Windows است، یادگیری PowerShell در کنار Bash ترکیب کامل‌تری ایجاد می‌کند.

داریوش حقیقی
نویسنده و توسعه‌دهنده

داریوش حقیقی

بیش از 20 سال تجربه در حوزه فناوری اطلاعات، طراحی سایت، سئو تکنیکال، مدیریت سرورهای لینوکس و ویندوز، توسعه وردپرس، برنامه‌نویسی، اتوماسیون و هوش مصنوعی.در djh.ir تلاش می‌کنم تجربیات واقعی پروژه‌های اجرایی، آموزش‌های کاربردی و راهکارهای عملی را با زبانی ساده و قابل استفاده منتشر کنم.

20+ سال تجربه
100+ پروژه اجرایی
1000+ ساعت آموزش

نظر و سوالتون رو اینجا بنویسید...

تماس در تلگرام