دمج أوامر Linux في Windows باستخدام PowerShell وWSL

سؤال نموذجي من مطوري Windows: "لماذا لا يوجد حتى الآن <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>؟. سواء كان ذلك انتقادا قويا less أو أدوات مألوفة grep أو sedيريد مطورو Windows الوصول بسهولة إلى هذه الأوامر في عملهم اليومي.

نظام Windows الفرعي لنظام التشغيل Linux (WSL) وقد خطت خطوة كبيرة إلى الأمام في هذا الصدد. فهو يسمح لك باستدعاء أوامر Linux من Windows عن طريق توكيلها من خلاله wsl.exe (على سبيل المثال، wsl ls). وعلى الرغم من أن هذا يعد تحسنا كبيرا، إلا أن هذا الخيار يعاني من عدد من العيوب.

  • إضافة في كل مكان wsl مملة وغير طبيعية.
  • لا تعمل مسارات Windows في الوسيطات دائمًا لأنه يتم تفسير الخطوط المائلة العكسية على أنها أحرف هروب بدلاً من فواصل الدليل.
  • لا تتم ترجمة مسارات Windows في الوسائط إلى نقطة التثبيت المقابلة في WSL.
  • لا يتم احترام الإعدادات الافتراضية في ملفات تعريف WSL ذات الأسماء المستعارة ومتغيرات البيئة.
  • إكمال مسار Linux غير مدعوم.
  • إكمال الأمر غير مدعوم.
  • إكمال الوسيطة غير معتمد.

ونتيجة لذلك، يتم التعامل مع أوامر Linux كمواطنين من الدرجة الثانية في نظام Windows، كما أن استخدامها أكثر صعوبة من الأوامر الأصلية. لتحقيق المساواة في حقوقهم، من الضروري حل المشاكل المذكورة.

أغلفة وظائف PowerShell

باستخدام أغلفة وظائف PowerShell، يمكننا إضافة إكمال الأوامر وإلغاء الحاجة إلى البادئات wslوترجمة مسارات Windows إلى مسارات WSL. المتطلبات الأساسية للقذائف:

  • لكل أمر Linux، يجب أن يكون هناك غلاف دالة واحد يحمل نفس الاسم.
  • يجب أن يتعرف الصدفة على مسارات Windows التي تم تمريرها كوسيطات ويقوم بتحويلها إلى مسارات WSL.
  • يجب أن تتصل القشرة wsl باستخدام أمر Linux المناسب لأي إدخال لخط الأنابيب وتمرير أي وسيطات سطر أوامر تم تمريرها إلى الوظيفة.

نظرًا لأنه يمكن تطبيق هذا النمط على أي أمر، فيمكننا تجريد تعريف هذه الأغلفة وإنشاءها ديناميكيًا من قائمة الأوامر المراد استيرادها.

# 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 (عن طريق إزالة أي أسماء مستعارة قد تتعارض مع الوظيفة أولاً).

تتكرر الوظيفة عبر وسيطات سطر الأوامر، وتحدد مسارات Windows باستخدام الأوامر Split-Path и Test-Pathثم يقوم بتحويل هذه المسارات إلى مسارات WSL. نقوم بتشغيل المسارات من خلال وظيفة مساعدة Format-WslArgument، والتي سوف نقوم بتعريفها لاحقاً. إنه يفلت من الأحرف الخاصة مثل المسافات والأقواس التي قد يتم تفسيرها بشكل خاطئ.

وأخيراً ننقل wsl إدخال خط الأنابيب وأي وسيطات سطر الأوامر.

باستخدام هذه الأغلفة، يمكنك استدعاء أوامر Linux المفضلة لديك بطريقة أكثر طبيعية دون إضافة بادئة wsl ودون القلق بشأن كيفية تحويل المسارات:

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

يتم عرض المجموعة الأساسية من الأوامر هنا، ولكن يمكنك إنشاء غلاف لأي أمر Linux بمجرد إضافته إلى القائمة. إذا قمت بإضافة هذا الرمز إلى الخاص بك ملف PowerShell، ستكون هذه الأوامر متاحة لك في كل جلسة PowerShell، تمامًا مثل الأوامر الأصلية!

الإعدادات الافتراضية

في Linux، من الشائع تحديد الأسماء المستعارة و/أو متغيرات البيئة في ملفات تعريف تسجيل الدخول، وتعيين المعلمات الافتراضية للأوامر المستخدمة بشكل متكرر (على سبيل المثال، alias ls=ls -AFh أو export LESS=-i). أحد عيوب الوكيل من خلال غلاف غير تفاعلي wsl.exe - أن الملفات الشخصية لم يتم تحميلها، وبالتالي فإن هذه الخيارات غير متاحة بشكل افتراضي (أي. ls في WSL و wsl ls سيتصرف بشكل مختلف مع الاسم المستعار المحدد أعلاه).

يوفر باورشيل $PSDefaultParameterValues، وهي آلية قياسية لتحديد المعلمات الافتراضية، ولكن فقط لأوامر cmdlets والوظائف المتقدمة. بالطبع، يمكننا إنشاء وظائف متقدمة من الأصداف الخاصة بنا، لكن هذا يؤدي إلى تعقيدات غير ضرورية (على سبيل المثال، يربط PowerShell أسماء المعلمات الجزئية (على سبيل المثال، -a يرتبط مع -ArgumentList)، والتي سوف تتعارض مع أوامر Linux التي تأخذ أسماء جزئية كوسيطات)، ولن يكون بناء جملة تحديد القيم الافتراضية هو الأنسب (تتطلب الوسائط الافتراضية اسم المعلمة في المفتاح، وليس اسم الأمر فقط) .

ومع ذلك، مع تعديل طفيف على قذائفنا، يمكننا تنفيذ نموذج مماثل ل $PSDefaultParameterValuesوتمكين الخيارات الافتراضية لأوامر Linux!

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. باش لديه قوة أدوات الإكمال التلقائي القابلة للبرمجة. يتيح لك 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وفرزها والهروب من الأحرف مثل المسافات والأقواس التي قد يتم تفسيرها بشكل خاطئ.

ونتيجة لذلك، ستستخدم قذائف أوامر Linux نفس الإكمال التلقائي تمامًا مثل bash! على سبيل المثال:

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

يوفر كل إكمال تلقائي قيمًا خاصة بالوسيطة السابقة، ويقرأ بيانات التكوين مثل المضيفين المعروفين من WSL!

<TAB> سوف يتنقل عبر المعلمات <Ctrl + пробел> سوف تظهر جميع الخيارات المتاحة.

بالإضافة إلى ذلك، نظرًا لأن لدينا الآن ميزة الإكمال التلقائي لـ bash، يمكنك إكمال مسارات Linux تلقائيًا مباشرةً في PowerShell!

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

في الحالات التي لا يؤدي فيها الإكمال التلقائي لـ bash إلى أي نتائج، يعود PowerShell إلى مسارات Windows الافتراضية للنظام. وبالتالي، في الممارسة العملية، يمكنك استخدام كلا المسارين في وقت واحد حسب تقديرك.

اختتام

باستخدام PowerShell وWSL، يمكننا دمج أوامر Linux في Windows كتطبيقات أصلية. ليست هناك حاجة للبحث عن إصدارات Win32 أو أدوات Linux المساعدة أو مقاطعة سير عملك بالانتقال إلى Linux Shell. فقط قم بتثبيت WSL، تهيئة الملف الشخصي باورشيل и قم بإدراج الأوامر التي تريد استيرادها! يعد الإكمال التلقائي الغني لمعلمات أوامر Linux وWindows ومسارات الملفات وظيفة غير متوفرة حتى في أوامر Windows الأصلية اليوم.

يتوفر كود المصدر الكامل الموضح أعلاه، بالإضافة إلى إرشادات إضافية لدمجه في سير العمل الخاص بك هنا.

ما هي أوامر Linux التي تجدها أكثر فائدة؟ ما الأشياء الشائعة الأخرى المفقودة عند العمل في Windows؟ اكتب في التعليقات أو على جيثب!

المصدر: www.habr.com

إضافة تعليق