การรวมคำสั่ง 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 และใช้งานยากกว่าคำสั่งแบบเนทิฟ เพื่อให้สิทธิของตนเท่าเทียมกันจำเป็นต้องแก้ไขปัญหาที่ระบุไว้

Wrapper ฟังก์ชัน PowerShell

ด้วยฟังก์ชัน Wrapper ของ PowerShell เราสามารถเพิ่มความสมบูรณ์ของคำสั่งและลดความจำเป็นในการเติมคำนำหน้าได้ wslแปลเส้นทาง Windows เป็นเส้นทาง WSL ข้อกำหนดพื้นฐานสำหรับเปลือกหอย:

  • สำหรับทุกคำสั่ง Linux จะต้องมี wrapper ฟังก์ชันหนึ่งรายการที่มีชื่อเดียวกัน
  • เชลล์ต้องรู้จักเส้นทาง Windows ที่ส่งเป็นอาร์กิวเมนต์ และแปลงเป็นเส้นทาง WSL
  • เปลือกควรโทร wsl ด้วยคำสั่ง Linux ที่เหมาะสมไปยังอินพุตไปป์ไลน์ใด ๆ และส่งผ่านอาร์กิวเมนต์บรรทัดคำสั่งใด ๆ ที่ส่งไปยังฟังก์ชัน

เนื่องจากรูปแบบนี้สามารถนำไปใช้กับคำสั่งใดก็ได้ เราจึงสามารถสรุปคำจำกัดความของ wrappers เหล่านี้ และสร้างแบบไดนามิกจากรายการคำสั่งที่จะนำเข้า

# 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 กำหนดคำสั่งนำเข้า จากนั้นเราจะสร้าง wrapper ฟังก์ชันแบบไดนามิกสำหรับแต่ละฟังก์ชันโดยใช้คำสั่ง Invoke-Expression (โดยลบนามแฝงที่อาจขัดแย้งกับฟังก์ชันออกก่อน)

ฟังก์ชันวนซ้ำอาร์กิวเมนต์บรรทัดคำสั่ง กำหนดเส้นทาง Windows โดยใช้คำสั่ง Split-Path и Test-Pathแล้วแปลงเส้นทางเหล่านี้เป็นเส้นทาง WSL เราดำเนินการเส้นทางผ่านฟังก์ชันตัวช่วย Format-WslArgumentซึ่งเราจะกำหนดในภายหลัง โดยจะหลีกอักขระพิเศษ เช่น ช่องว่างและวงเล็บที่อาจตีความหมายผิด

ในที่สุดเราก็ถ่ายทอด wsl อินพุตไปป์ไลน์และอาร์กิวเมนต์บรรทัดคำสั่งใด ๆ

ด้วย wrapper เหล่านี้ คุณสามารถเรียกใช้คำสั่ง 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 จะทำงานแตกต่างออกไปกับนามแฝงที่กำหนดไว้ข้างต้น)

PowerShell จัดให้ $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. Bash มีพลัง เครื่องมือเติมข้อความอัตโนมัติที่ตั้งโปรแกรมได้. WSL อนุญาตให้คุณโทรทุบตีจาก PowerShell หากเราสามารถลงทะเบียนการเสร็จสิ้นอาร์กิวเมนต์สำหรับ wrappers ฟังก์ชัน 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 แค่ ติดตั้ง WSL, กำหนดค่า โปรไฟล์ PowerShell и รายการคำสั่งที่คุณต้องการนำเข้า! การเติมข้อความอัตโนมัติที่สมบูรณ์สำหรับพารามิเตอร์คำสั่ง Linux และ Windows และเส้นทางของไฟล์เป็นฟังก์ชันการทำงานที่ไม่มีในคำสั่ง Windows ดั้งเดิมในปัจจุบัน

มีซอร์สโค้ดฉบับสมบูรณ์ที่อธิบายไว้ข้างต้น รวมถึงแนวทางเพิ่มเติมในการรวมไว้ในเวิร์กโฟลว์ของคุณ ที่นี่.

คำสั่ง Linux ใดที่คุณพบว่ามีประโยชน์มากที่สุด? มีอะไรทั่วไปอีกบ้างที่หายไปเมื่อทำงานใน Windows? เขียนในความคิดเห็นหรือ บน GitHub!

ที่มา: will.com

เพิ่มความคิดเห็น