کالبدشکافی یک cd اشتباهی...

چند روز پیش می‌خواستم که یه su - خیلی سریع انجام بِدم، پس با عجله یه پنجره‌ی جدید از Konsole رو باز کردم و داخلش cd - رو نوشتم و Enter زدم… گفتش که bash: cd: OLDPWD not set :| ها؟! :)) اولش یه لحظه‌ی کوتاهی جا خوردم و خُب سریع متوجه اشتباهم داخل تایپ دستور شدم، ولی بعدش اون پیغام خطا توجهم رو جلب کرد… دوباره یه نگاه به قیافه‌ی cd - انداختم و با توجه به اینکه خطا، داخل یه پنجره‌ی کاملاً جدید رخ داده بود، یادم به یه مقاله‌ای افتاد که چندین ماه پیش یه مروری روش داشتم، ولی دیگه تقریباً فراموشش کرده بودم: نحوه‌ی بَک (back) زدن داخل شِل (shell) لینوکس

استفاده از cd - خیلی ساده است؛ مثلاً فرض کنید که داخل /usr/lib/gcc/avr هستید و دارید به کاروزندگی معمول خودتون رسیدگی می‌کنید:

demo@debian:/usr/lib/gcc/avr$ 

 حالا یهو نیاز می‌شه که به /etc/udev بِرید و ترتیب انجام یه کاری رو بِدید:

demo@debian:/usr/lib/gcc/avr$ cd /etc/udev/
demo@debian:/etc/udev$ 

بعدش وقتی که کارِتون داخل /etc/udev تموم شد، می‌خواید که برگردید به همون مسیر قبلی (/usr/lib/gcc/avr) و کاروزندگی معمول خودتون رو ادامه بِدید. این «برگشتن» به دایرِکتوری (directory) قبلی، به‌سادگی با cd - انجام می‌شه:

demo@debian:/etc/udev$ cd -
/usr/lib/gcc/avr
demo@debian:/usr/lib/gcc/avr$

حالا اگه از قضای روزگار، متوجه بشید که یه تغییر دیگه هم داخل /etc/udev نیاز هست و لازمه تا مجدداً به اونجا برگردید، دوباره راحت می‌تونید از cd - استفاده کنید:

demo@debian:/usr/lib/gcc/avr$ cd -
/etc/udev
demo@debian:/etc/udev$

رفتاری که cd - از خودش نشون می‌ده، دقیقاً مثل همون رفتاریه که دکمه‌ی Back داخل نرم‌افزارهای گرافیکی مدیریت فایل داره: داخل این نرم‌افزارها هم اگه در حال کار داخل یه پوشه یا فولدِر (folder) باشید و یهو مسیر فعالیت خودتون رو عوض کنید (مثلاً با استفاده از یه نوع shortcut یا با تایپ مسیر جدید)، بعدش می‌تونید با زدن دکمه‌ی Back به همون مسیر اولیه برگردید.

اما bash: cd: OLDPWD not set...

اون خطایی که اول مقاله ازش صحبت کردم، جالبه. وقتی که داخل یه پنجره‌ی جدید Konsole، درجا cd - رو اجرا کنید، خطا می‌ده:

demo@debian:~$ cd -
bash: cd: OLDPWD not set
demo@debian:~$ 

خُب البته خطا دادن برای درخواست بَک یا بازگشت به مسیر قبلی داخل یه پنجره‌ی جدید، خیلی منطقی و متینه دیگه… اگه یه پنجره از یه نرم‌افزار مدیریت فایل هم باز کنیم، همون اول کاری، دکمه‌ی Back غیرفعاله: «مسیر قبلی» و اصلاً هیچ سابقه‌ای وجود نداره که بخوایم بهش «برگردیم»! :) به‌هرحال، اون OLDPWD توجه من رو جلب کرد... :) دستور pwd دایرکتوری فعلی رو پرینت می‌کنه، پس دیگه اون PWD موجود داخل OLDPWD براتون کاملاً معنی‌دار می‌شه… اگه man bash رو اجرا کنید، داخل بخش Shell Variables در راهنمای Bash، واسه‌ی OLDPWD و PWD تعریف‌هایی پیدا می‌شه:

OLDPWD The previous working directory as set by the cd command.
PWD    The current working directory as set by the cd command.

پس متغیر OLDPWD همیشه قراره مسیر دایرکتوری «قبلی» رو داخل خودش نگه داره و PWD هم همیشه مسیر دایرکتوری «فعلی»… اجرای دستور help cd یا cd --help، خیلی واضح بیان می‌کنه که آرگومان - موجود در cd - به $OLDPWD تبدیل می‌شه:

demo@debian:~$ cd --help
cd: cd [-L|[-P [-e]] [-@]] [dir]
    Change the shell working directory.
    
    Change the current directory to DIR.  The default DIR is the value of the
    HOME shell variable. If DIR is "-", it is converted to $OLDPWD.
    
    [...]

پس در واقع، وقتی cd - رو اجرا کنیم، برنامه‌ی cd می‌آد فرض رو بر این می‌ذاره که ما cd $OLDPWD رو اجرا کرده‌ایم. این مورد، با نگاه کردن به اطراف خط 346 داخل سورس‌کُد cd در Bash-5.2 هم کاملاً مشخص می‌شه:

else if (list->word->word[0] == '-' && list->word->word[1] == '\0')
{
	/* This is `cd -', equivalent to `cd $OLDPWD' */
	dirname = get_string_value ("OLDPWD");

	if (dirname == 0)
	{
		builtin_error (_("OLDPWD not set"));
		return (EXECUTION_FAILURE);
	}

/* ... */

اتفاقاً همین‌جا، اون پیغام خطای خودمون رو هم می‌بینیم دیگه! :) از کد بالا معلومه که وقتی cd - رو اجرا می‌کنیم، مقدار متغیر محیطی OLDPWD درخواست می‌شه و داخل متغیر dirname قرار می‌گیره. حالا اگه ما یه پنجره‌ی جدید باز کرده باشیم، متغیر محیطی OLDPWD هنوز تعریف نشده و مقداری نداره:

demo@debian:~$ printenv OLDPWD
demo@debian:~$ 

پس فراخوانی get_string_value("OLDPWD") مقدار NULL رو بَرمی‌گردونه و اون if (dirname == 0) { ... } ترتیب پیغام خطا رو می‌ده...

حالا اگه cd رو طوری و زمانی فراخوانی کرده باشیم که اینجا خطایی رخ نده و بِره سراغ تغییر دایرکتوری، از همون متغیر dirname برای مسیر مقصد استفاده می‌کنه. داخل سورس‌کد cd که پایین‌تر بریم، اطراف خط 416، کدهای زیر رو داریم:

/* When we get here, DIRNAME is the directory to change to.  If we
   chdir successfully, just return. */
if (change_to_directory (dirname, no_symlinks, xattrflag))
{
	if (lflag & LCD_PRINTPATH)
		printf ("%s\n", dirname);
	return (bindpwd (no_symlinks));
}

اینجا اون جاییه که دیگه می‌خواد عملیات تغییر دایرکتوری رو انجام بِده. اگه dirname در واقع آدرس یه دایرکتوری باشه و عملیات change_to_directory() با موفقیت انجام بشه، تابع bindpwd() موقع خروج، فراخوانی می‌شه؛ تابعی که بالاتر، توی خط 136 تعریف شده. از تعریف bindpwd() ظاهراً این‌طور بَرمی‌آد که ما از این تابع خارج نمی‌شیم، مگر اینکه مقدار فعلی PWD رو ریخته باشیم داخل OLDPWD؛ پس هر بار اجرای موفق cd در این حالت، مقدار OLDPWD رو آپدِیت می‌کنه. خط 160 و 162 رو نگاه کنید:

pwdvar = get_string_value ("PWD");

tvar = bind_variable ("OLDPWD", pwdvar, 0);

پس اول مقدار فعلی متغیر محیطی PWD رو می‌گیره و می‌ریزه داخل OLDPWD. بعدش داخل خط 172 مقدار PWD رو با setpwd(dirname) آپدیت می‌کنه و در نهایت، توی خط 180، یه return داریم. این بازی‌هایی که Bash داره با متغیرهای محیطی انجام می‌ده و استفاده‌های مختلفی که ما هم می‌تونیم از این متغیرها ببَریم، می‌تونه در نوع خودش جالب باشه… :)

چند تا نکته

توجه دارید که بَک با آپ (Up) فرق داره دیگه… همون‌طوری که داخل نرم‌افزارهای مدیریت فایل، یه دکمه‌ی Back هست و یه دکمه‌ی Up، داخل شِل هم جفت این کارکردها وجود دارن… آپ، احتمالاً جزو اولین چیزهایی هست که باید موقع کار کردن با شِل یاد بگیرید دیگه… آپ، همیشه مسیر رو به والد دایرکتوری فعلی تغییر می‌ده؛ یعنی داخل هر فولدری که باشیم، با آپ کردن، به والد اون فولدر می‌ریم. عملیات آپ، با cd .. انجام می‌شه. مثلاً اگر داخل /etc/udev باشیم، اجرای آپ باید ما رو ببره به /etc:

demo@debian:/etc/udev$ cd ..
demo@debian:/etc$ 

قبلاً گفتیم که هر بار اجرای موفقیت‌آمیز cd، مقدار متغیر OLDPWD رو آپدیت می‌کنه؛ پس اگه بعد از این دستور آپ، داخل /etc یه بَک بزنیم، این بَک ما رو به /etc/udev بَرمی‌گردونه و دیگه کلاً اون مسیر /usr/lib/gcc/avr اول مقاله به فراموشی سپرده می‌شه:

demo@debian:/usr/lib/gcc/avr$ cd /etc/udev/
demo@debian:/etc/udev$ echo $OLDPWD 
/usr/lib/gcc/avr
demo@debian:/etc/udev$ cd ..
demo@debian:/etc$ echo $OLDPWD 
/etc/udev
demo@debian:/etc$ cd -
/etc/udev
demo@debian:/etc/udev$ 

همین‌طور باید توجه کنیم که برای cd اهمیتی نداره که مقدار OLDPWD برای بَک زدن، معنی‌دار هست یا نه… مثلاً اگه cd . رو اجرا کنیم، مقدار متغیر OLDPWD با مقدار PWD برابر می‌شه! :)) حالا دیگه اجرای cd -، ما رو داخل دایرکتوری فعلی نگه می‌داره! :))

demo@debian:/etc/udev$ echo $OLDPWD 
/etc
demo@debian:/etc/udev$ cd .
demo@debian:/etc/udev$ echo $OLDPWD 
/etc/udev
demo@debian:/etc/udev$ cd -
/etc/udev
demo@debian:/etc/udev$ 

راستی تا داریم از cd حرف می‌زنیم، خوبه این نکته‌ی تقریباً بی‌ربط رو هم اضافه کنم که اگر مسیری برای مقصد cd تعیین نشه، cd به‌صورت پیش‌فرض به دایرکتوری home کاربر فعلی می‌ره؛ پس اجرای یه cd خالی با اجرای cd ~ فرقی نداره و جفتشون ما رو به home می‌بَرن. (توجه داشته باشید که کاراکتر ~ در دستور cd ~، کاراکتر tilde هست که یه‌جورایی شبیه مَد خودمونه و داخل کیبورد، معمولاً اون بالا سمت چپ در کنار کلید 1 قرار داره.)

demo@debian:/usr/lib/gcc/avr$ cd
demo@debian:~$ pwd
/home/demo
demo@debian:~$ 

اگه به خط 320 سورس‌کد cd نگاه کنید، می‌بینید که داخل این حالت، مقدار متغیر محیطی HOME درخواست شده و داخل dirname ریخته می‌شه:

if (list == 0)
{
	/* `cd' without arguments is equivalent to `cd $HOME' */
	dirname = get_string_value ("HOME");

/* ... */

البته این نکته کاملاً هم بی‌ربط نیست ها… قبلاً دیدیم که اجرای cd -، شبیه به اجرای cd $OLDPWD هست… «شبیه» هست، ولی به‌لحاظ رفتاری، دقیقاً معادل نیست… همون وقتی که cd - به ما خطا می‌ده که متغیر OLDPWD تعریف نشده، اگه خودمون cd $OLDPWD رو اجرا کنیم، با هیچ خطایی مواجه نمی‌شیم؛ چرا؟ ازاونجایی‌که متغیر OLDPWD تعریف نشده، پس دستور cd $OLDPWD با دستور cd خالی معادل هست و ما رو به home می‌بَره. توجه می‌کنید که وقتی یک بار دستور cd با موفقیت اجرا بشه، دیگه OLDPWD رو خودش تعریف می‌کنه:

demo@debian:~$ cd -
bash: cd: OLDPWD not set
demo@debian:~$ cd $OLDPWD
demo@debian:~$ echo $OLDPWD 
/home/demo
demo@debian:~$ 

پس در واقع، وقتی که OLDPWD اصلاً تعریف نشده باشه، دیگه کلاً هیچ اهمیت و مفهومی داخل دستور cd $OLDPWD نداره و می‌تونیم با هر متغیر تخیلی دیگه‌ای جایگزینش کنیم و همین نتیجه‌ی بالا رو بگیریم. داخل مثال زیر، از متغیر تخیلی ALAKI استفاده کرده‌ام. می‌بینید که نتیجه، هیچ تفاوتی نداره:

demo@debian:~$ cd -
bash: cd: OLDPWD not set
demo@debian:~$ echo $ALAKI

demo@debian:~$ cd $ALAKI
demo@debian:~$ echo $OLDPWD 
/home/demo
demo@debian:~$ 

جمع‌بندی

همون‌طوری که نرم‌افزارهای مدیریت فایل به ما اجازه‌ی بَک کردن و بازگشت به مسیر قبلی رو می‌دَن، شِلِ لینوکس هم این قابلیت رو با استفاده از دستور cd - به ما ارائه می‌کنه. هرچند داخل واسط‌های گرافیکی که پنجره دارن و محدود به یه دونه صفحه‌ی سیاه نیستن، چنین قابلیتی ممکنه خیلی مفید به نظر نرسه، اما در عوض، داشتن مهارت در شِل موقع ارتباط با سیستم‌هایی که واسط گرافیکی ندارن، می‌تونه خیلی مؤثر باشه. خُب اتفاقاً داخل بحث Embedded Systems خیلی به این نوع سیستم‌ها بَرمی‌خوریم دیگه؛ مثلاً همین cd - رو ممکنه که بشه داخل BusyBox هم ازش استفاده کرد... کافیه که یه نگاهی به سورس‌کدش بندازید تا OLDPWD آشنای خودمون رو داخل تعریف تابع cdcmd() ببینید! :) علاوه‌بر این‌ها، موقعی که به واسطه‌ی SSH یا Telnet هم با یه سِروِر ارتباط برقرار می‌کنید، چنین قابلیت‌هایی ممکنه که به کار بیاد.

حالا راستش جدا از اینکه اصلاً این قابلیت به کار می‌آد یا نه، من cd - رو یه بهونه‌ای کردم تا داخل این مقاله، یه سَری به سورس‌کد Bash هم بزنم و یه مقدار اندکی از کد رو بررسی کنم، بلکه برای افراد علاقه‌مند به زبان C و برنامه‌نویسی سیستمی و سطح پایین، انگیزه و تشویقی بشه تا اینکه از بررسی کدهای واقعی که به‌وفور در طبیعت یافت می‌شه، هراس کمتری داشته باشن. در همین راستا، یه‌کمی با متغیرهای شِل هم بازی کردم تا مفاهیم شِل، کم‌کم برای افرادی جا بیُفته که شاید به‌تازگی وارد دنیای مرموز و تاریکِ شِل شده‌اند… :)

علاوه‌بر اون مرجعی که واسه‌ی cd - در ابتدای مقاله معرفی کردم، ممکنه که نگاه کردن به این مقاله‌ی ویکی‌پدیا و همین‌طور، بحث‌هایی که در پاسخ به یه سؤال در سایت Super User مطرح شده، برای شما جالب باشه. اگر درباره‌ی دستور su - که همه‌ی این جنجال‌ها رو ایجاد کرد، اطلاعات بیشتری نیاز دارید، شاید بد نباشه که یه سَری به این سؤال و همین‌طور، این یکی سؤال از سایت Unix & Linux Stack Exchange بزنید.