ادغام دستورات لینوکس در ویندوز با استفاده از PowerShell و WSL

یک سوال معمولی از توسعه دهندگان ویندوز: "چرا هنوز وجود ندارد <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?. خواه این یک سوایپ قدرتمند باشد less یا ابزار آشنا grep یا sed، توسعه دهندگان ویندوز خواهان دسترسی آسان به این دستورات در کارهای روزمره خود هستند.

زیرسیستم ویندوز برای لینوکس (WSL) گام بزرگی در این زمینه برداشته است. این امکان را به شما می دهد تا با پروکسی کردن دستورات لینوکس از ویندوز آنها را فراخوانی کنید wsl.exe (مثلاً wsl ls). اگرچه این یک پیشرفت قابل توجه است، اما این گزینه از تعدادی معایب رنج می برد.

  • اضافه همه جا حاضر wsl خسته کننده و غیر طبیعی
  • مسیرهای ویندوز در آرگومان ها همیشه کار نمی کنند زیرا اسلش های برگشتی به جای جداکننده دایرکتوری به عنوان کاراکترهای فرار تفسیر می شوند.
  • مسیرهای ویندوز در آرگومان ها به نقطه اتصال مربوطه در WSL ترجمه نمی شوند.
  • تنظیمات پیش‌فرض در پروفایل‌های WSL با نام مستعار و متغیرهای محیطی رعایت نمی‌شوند.
  • تکمیل مسیر لینوکس پشتیبانی نمی شود.
  • تکمیل فرمان پشتیبانی نمی شود.
  • تکمیل آرگومان پشتیبانی نمی شود.

در نتیجه، با دستورات لینوکس مانند شهروندان درجه دوم تحت ویندوز رفتار می‌شود و استفاده از آنها نسبت به دستورات بومی دشوارتر است. برای یکسان سازی حقوق آنها باید مشکلات ذکر شده حل شود.

بسته بندی های تابع PowerShell

با بسته‌بندی‌های تابع PowerShell، می‌توانیم تکمیل دستور را اضافه کنیم و نیاز به پیشوندها را از بین ببریم wsl، مسیرهای ویندوز را به مسیرهای WSL ترجمه می کند. الزامات اولیه برای پوسته:

  • برای هر دستور لینوکس باید یک تابع بسته بندی با همان نام وجود داشته باشد.
  • پوسته باید مسیرهای ویندوز پاس شده را به عنوان آرگومان تشخیص دهد و آنها را به مسیرهای WSL تبدیل کند.
  • پوسته باید تماس بگیرد wsl با دستور لینوکس مناسب به هر ورودی خط لوله و ارسال هر آرگومان خط فرمان که به تابع ارسال می شود.

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

# The commands to import.
$commands = "awk", "emacs", "grep", "head", "less", "ls", "man", "sed", "seq", "ssh", "tail", "vim"
 
# Register a function for each command.
$commands | ForEach-Object { Invoke-Expression @"
Remove-Alias $_ -Force -ErrorAction Ignore
function global:$_() {
    for (`$i = 0; `$i -lt `$args.Count; `$i++) {
        # If a path is absolute with a qualifier (e.g. C:), run it through wslpath to map it to the appropriate mount point.
        if (Split-Path `$args[`$i] -IsAbsolute -ErrorAction Ignore) {
            `$args[`$i] = Format-WslArgument (wsl.exe wslpath (`$args[`$i] -replace "", "/"))
        # If a path is relative, the current working directory will be translated to an appropriate mount point, so just format it.
        } elseif (Test-Path `$args[`$i] -ErrorAction Ignore) {
            `$args[`$i] = Format-WslArgument (`$args[`$i] -replace "", "/")
        }
    }
 
    if (`$input.MoveNext()) {
        `$input.Reset()
        `$input | wsl.exe $_ (`$args -split ' ')
    } else {
        wsl.exe $_ (`$args -split ' ')
    }
}
"@
}

فهرست $command دستورات واردات را تعریف می کند. سپس با استفاده از دستور برای هر یک از آنها به صورت پویا یک تابع پوشش تولید می کنیم Invoke-Expression (ابتدا با حذف هر نام مستعار که با تابع مغایرت دارد).

این تابع روی آرگومان های خط فرمان تکرار می شود، مسیرهای ویندوز را با استفاده از دستورات تعیین می کند Split-Path и Test-Pathو سپس این مسیرها را به مسیرهای WSL تبدیل می کند. ما مسیرها را از طریق یک تابع کمکی اجرا می کنیم Format-WslArgumentکه در ادامه به تعریف آن خواهیم پرداخت. از کاراکترهای خاص مانند فاصله و پرانتز که در غیر این صورت اشتباه تعبیر می‌شوند فرار می‌کند.

در نهایت انتقال می دهیم wsl ورودی خط لوله و هر آرگومان خط فرمان.

با استفاده از این بسته‌بندی‌ها می‌توانید دستورات لینوکس مورد علاقه خود را به روشی طبیعی‌تر و بدون افزودن پیشوند فراخوانی کنید wsl و بدون نگرانی در مورد نحوه تبدیل مسیرها:

  • man bash
  • less -i $profile.CurrentUserAllHosts
  • ls -Al C:Windows | less
  • grep -Ein error *.log
  • tail -f *.log

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

تنظیمات پیش فرض

در لینوکس، تعریف نام مستعار و/یا متغیرهای محیطی در نمایه‌های ورود، تنظیم پارامترهای پیش‌فرض برای دستورات پرکاربرد (مثلاً alias ls=ls -AFh یا export LESS=-i). یکی از معایب پروکسی از طریق پوسته غیر تعاملی wsl.exe - که نمایه ها بارگذاری نمی شوند، بنابراین این گزینه ها به طور پیش فرض در دسترس نیستند (یعنی ls در WSL و wsl ls با نام مستعار تعریف شده در بالا متفاوت رفتار خواهد کرد).

PowerShell فراهم می کند $PSDefaultParameterValuesیک مکانیسم استاندارد برای تعریف پارامترهای پیش فرض، اما فقط برای cmdlet ها و توابع پیشرفته. البته، ما می‌توانیم توابع پیشرفته‌ای را از پوسته‌های خود بسازیم، اما این باعث ایجاد پیچیدگی‌های غیر ضروری می‌شود (به عنوان مثال، PowerShell نام پارامترهای جزئی را به هم مرتبط می‌کند (به عنوان مثال، -a با -ArgumentList) که با دستورات لینوکس که نام‌های جزئی را به عنوان آرگومان می‌گیرند در تضاد است، و نحو برای تعریف مقادیر پیش‌فرض مناسب‌ترین نخواهد بود (آگومان‌های پیش‌فرض به نام پارامتر در کلید نیاز دارند، نه فقط نام فرمان) .

با این حال، با کمی تغییر در پوسته های خود، می توانیم مدلی مشابه را پیاده سازی کنیم $PSDefaultParameterValuesو گزینه های پیش فرض را برای دستورات لینوکس فعال کنید!

function global:$_() {
    …
 
    `$defaultArgs = ((`$WslDefaultParameterValues.$_ -split ' '), "")[`$WslDefaultParameterValues.Disabled -eq `$true]
    if (`$input.MoveNext()) {
        `$input.Reset()
        `$input | wsl.exe $_ `$defaultArgs (`$args -split ' ')
    } else {
        wsl.exe $_ `$defaultArgs (`$args -split ' ')
    }
}

گذراندن $WslDefaultParameterValues به خط فرمان، پارامترها را از طریق ارسال می کنیم wsl.exe. در ادامه نحوه افزودن دستورالعمل‌ها به نمایه PowerShell خود برای پیکربندی تنظیمات پیش‌فرض نشان داده می‌شود. حالا ما می توانیم آن را انجام دهیم!

$WslDefaultParameterValues["grep"] = "-E"
$WslDefaultParameterValues["less"] = "-i"
$WslDefaultParameterValues["ls"] = "-AFh --group-directories-first"

از آنجایی که پارامترها پس از مدل سازی می شوند $PSDefaultParameterValues، تو می توانی غیرفعال کردن آنها آسان است به طور موقت با نصب کلید "Disabled" به معنا $true. مزیت اضافی جدول هش جداگانه، امکان غیرفعال کردن است $WslDefaultParameterValues جدا از $PSDefaultParameterValues.

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

PowerShell به شما امکان می دهد تا تریلرهای آرگومان را با استفاده از دستور ثبت کنید Register-ArgumentCompleter. Bash قدرتمند است ابزارهای تکمیل خودکار قابل برنامه ریزی. WSL به شما امکان می دهد bash را از PowerShell فراخوانی کنید. اگر بتوانیم تکمیل‌های آرگومان را برای بسته‌بندی‌های تابع PowerShell خود ثبت کنیم و bash را برای تولید تکمیل‌ها فراخوانی کنیم، تکمیل آرگومان کامل را با همان دقت خود bash دریافت می‌کنیم!

# Register an ArgumentCompleter that shims bash's programmable completion.
Register-ArgumentCompleter -CommandName $commands -ScriptBlock {
    param($wordToComplete, $commandAst, $cursorPosition)
 
    # Map the command to the appropriate bash completion function.
    $F = switch ($commandAst.CommandElements[0].Value) {
        {$_ -in "awk", "grep", "head", "less", "ls", "sed", "seq", "tail"} {
            "_longopt"
            break
        }
 
        "man" {
            "_man"
            break
        }
 
        "ssh" {
            "_ssh"
            break
        }
 
        Default {
            "_minimal"
            break
        }
    }
 
    # Populate bash programmable completion variables.
    $COMP_LINE = "`"$commandAst`""
    $COMP_WORDS = "('$($commandAst.CommandElements.Extent.Text -join "' '")')" -replace "''", "'"
    for ($i = 1; $i -lt $commandAst.CommandElements.Count; $i++) {
        $extent = $commandAst.CommandElements[$i].Extent
        if ($cursorPosition -lt $extent.EndColumnNumber) {
            # The cursor is in the middle of a word to complete.
            $previousWord = $commandAst.CommandElements[$i - 1].Extent.Text
            $COMP_CWORD = $i
            break
        } elseif ($cursorPosition -eq $extent.EndColumnNumber) {
            # The cursor is immediately after the current word.
            $previousWord = $extent.Text
            $COMP_CWORD = $i + 1
            break
        } elseif ($cursorPosition -lt $extent.StartColumnNumber) {
            # The cursor is within whitespace between the previous and current words.
            $previousWord = $commandAst.CommandElements[$i - 1].Extent.Text
            $COMP_CWORD = $i
            break
        } elseif ($i -eq $commandAst.CommandElements.Count - 1 -and $cursorPosition -gt $extent.EndColumnNumber) {
            # The cursor is within whitespace at the end of the line.
            $previousWord = $extent.Text
            $COMP_CWORD = $i + 1
            break
        }
    }
 
    # Repopulate bash programmable completion variables for scenarios like '/mnt/c/Program Files'/<TAB> where <TAB> should continue completing the quoted path.
    $currentExtent = $commandAst.CommandElements[$COMP_CWORD].Extent
    $previousExtent = $commandAst.CommandElements[$COMP_CWORD - 1].Extent
    if ($currentExtent.Text -like "/*" -and $currentExtent.StartColumnNumber -eq $previousExtent.EndColumnNumber) {
        $COMP_LINE = $COMP_LINE -replace "$($previousExtent.Text)$($currentExtent.Text)", $wordToComplete
        $COMP_WORDS = $COMP_WORDS -replace "$($previousExtent.Text) '$($currentExtent.Text)'", $wordToComplete
        $previousWord = $commandAst.CommandElements[$COMP_CWORD - 2].Extent.Text
        $COMP_CWORD -= 1
    }
 
    # Build the command to pass to WSL.
    $command = $commandAst.CommandElements[0].Value
    $bashCompletion = ". /usr/share/bash-completion/bash_completion 2> /dev/null"
    $commandCompletion = ". /usr/share/bash-completion/completions/$command 2> /dev/null"
    $COMPINPUT = "COMP_LINE=$COMP_LINE; COMP_WORDS=$COMP_WORDS; COMP_CWORD=$COMP_CWORD; COMP_POINT=$cursorPosition"
    $COMPGEN = "bind `"set completion-ignore-case on`" 2> /dev/null; $F `"$command`" `"$wordToComplete`" `"$previousWord`" 2> /dev/null"
    $COMPREPLY = "IFS=`$'n'; echo `"`${COMPREPLY[*]}`""
    $commandLine = "$bashCompletion; $commandCompletion; $COMPINPUT; $COMPGEN; $COMPREPLY" -split ' '
 
    # Invoke bash completion and return CompletionResults.
    $previousCompletionText = ""
    (wsl.exe $commandLine) -split 'n' |
    Sort-Object -Unique -CaseSensitive |
    ForEach-Object {
        if ($wordToComplete -match "(.*=).*") {
            $completionText = Format-WslArgument ($Matches[1] + $_) $true
            $listItemText = $_
        } else {
            $completionText = Format-WslArgument $_ $true
            $listItemText = $completionText
        }
 
        if ($completionText -eq $previousCompletionText) {
            # Differentiate completions that differ only by case otherwise PowerShell will view them as duplicate.
            $listItemText += ' '
        }
 
        $previousCompletionText = $completionText
        [System.Management.Automation.CompletionResult]::new($completionText, $listItemText, 'ParameterName', $completionText)
    }
}
 
# Helper function to escape characters in arguments passed to WSL that would otherwise be misinterpreted.
function global:Format-WslArgument([string]$arg, [bool]$interactive) {
    if ($interactive -and $arg.Contains(" ")) {
        return "'$arg'"
    } else {
        return ($arg -replace " ", " ") -replace "([()|])", ('$1', '`$1')[$interactive]
    }
}

کد بدون درک برخی از عملکردهای داخلی bash کمی متراکم است، اما اساسا کاری که ما انجام می دهیم این است:

  • ثبت یک تکمیل کننده آرگومان برای همه پوشش های تابع ما با ارسال یک لیست $commands در پارامتر -CommandName برای Register-ArgumentCompleter.
  • ما هر دستور را به تابع پوسته ای که bash برای تکمیل خودکار استفاده می کند نگاشت می کنیم (برای تعریف مشخصات تکمیل خودکار، bash از آن استفاده می کند. $F، مخفف برای complete -F <FUNCTION>).
  • تبدیل آرگومان های PowerShell $wordToComplete, $commandAst и $cursorPosition به قالب مورد انتظار توابع تکمیل خودکار bash مطابق با مشخصات تکمیل خودکار قابل برنامه ریزی بش.
  • ما یک خط فرمان برای انتقال به آن می نویسیم wsl.exe، که اطمینان حاصل می کند که محیط به درستی تنظیم شده است، تابع تکمیل خودکار مناسب را فراخوانی می کند و نتایج را به صورت خط به خط خروجی می دهد.
  • بعد زنگ میزنیم wsl با خط فرمان، خروجی را با جداکننده های خط جدا می کنیم و برای هر کدام تولید می کنیم CompletionResults، مرتب کردن آنها و فرار از کاراکترهایی مانند فاصله و پرانتز که در غیر این صورت اشتباه تفسیر می شوند.

در نتیجه، پوسته های فرمان لینوکس ما دقیقاً از همان تکمیل خودکار bash استفاده می کنند! مثلا:

  • ssh -c <TAB> -J <TAB> -m <TAB> -O <TAB> -o <TAB> -Q <TAB> -w <TAB> -b <TAB>

هر تکمیل خودکار مقادیر خاص آرگومان قبلی را فراهم می‌کند و داده‌های پیکربندی مانند میزبان‌های شناخته شده از WSL را می‌خواند!

<TAB> در میان پارامترها چرخه خواهد شد. <Ctrl + пробел> تمام گزینه های موجود را نشان می دهد.

به علاوه، از آنجایی که اکنون تکمیل خودکار bash داریم، می‌توانید مسیرهای لینوکس را مستقیماً در PowerShell تکمیل کنید!

  • less /etc/<TAB>
  • ls /usr/share/<TAB>
  • vim ~/.bash<TAB>

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

نتیجه

با استفاده از PowerShell و WSL، می‌توانیم دستورات لینوکس را به عنوان برنامه‌های بومی در ویندوز ادغام کنیم. نیازی به جستجوی ساخت‌های Win32 یا ابزارهای لینوکس نیست یا با رفتن به پوسته لینوکس، جریان کار خود را قطع کنید. فقط WSL را نصب کنید، پیکربندی کنید پروفایل PowerShell и دستوراتی را که می خواهید وارد کنید فهرست کنید! تکمیل خودکار غنی برای پارامترهای دستوری لینوکس و ویندوز و مسیرهای فایل، عملکردی است که امروزه حتی در دستورات بومی ویندوز موجود نیست.

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

کدام دستورات لینوکس را مفیدتر می دانید؟ چه چیزهای رایج دیگری هنگام کار در ویندوز از دست می رود؟ در نظرات بنویسید یا در GitHub!

منبع: www.habr.com

اضافه کردن نظر