Tích hợp lệnh Linux vào Windows bằng PowerShell và WSL

Một câu hỏi điển hình của các nhà phát triển Windows: “Tại sao vẫn chưa có <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?. Cho dù đó là một cú vuốt mạnh mẽ less hoặc những công cụ quen thuộc grep hoặc sedCác nhà phát triển Windows muốn dễ dàng truy cập vào các lệnh này trong công việc hàng ngày của họ.

Hệ thống con Windows cho Linux (WSL) đã có một bước tiến lớn về mặt này. Nó cho phép bạn gọi các lệnh Linux từ Windows bằng cách ủy quyền chúng thông qua wsl.exe (ví dụ: wsl ls). Mặc dù đây là một cải tiến đáng kể nhưng tùy chọn này cũng có một số nhược điểm.

  • Bổ sung phổ biến wsl tẻ nhạt và thiếu tự nhiên.
  • Đường dẫn Windows trong đối số không phải lúc nào cũng hoạt động vì dấu gạch chéo ngược được hiểu là ký tự thoát thay vì dấu phân cách thư mục.
  • Đường dẫn Windows trong đối số không được dịch sang điểm gắn kết tương ứng trong WSL.
  • Cài đặt mặc định không được tôn trọng trong cấu hình WSL với bí danh và biến môi trường.
  • Hoàn thành đường dẫn Linux không được hỗ trợ.
  • Hoàn thành lệnh không được hỗ trợ.
  • Hoàn thành đối số không được hỗ trợ.

Kết quả là, các lệnh Linux được coi như các công dân hạng hai trong Windows—và khó sử dụng hơn các lệnh gốc. Để bình đẳng hóa quyền lợi của họ, cần phải giải quyết các vấn đề đã nêu.

Trình bao bọc hàm PowerShell

Với trình bao bọc hàm PowerShell, chúng ta có thể thêm tính năng hoàn thành lệnh và loại bỏ nhu cầu về tiền tố wsl, dịch đường dẫn Windows thành đường dẫn WSL. Yêu cầu cơ bản đối với shell:

  • Đối với mỗi lệnh Linux phải có một trình bao bọc hàm có cùng tên.
  • Shell phải nhận ra các đường dẫn Windows được truyền dưới dạng đối số và chuyển đổi chúng thành đường dẫn WSL.
  • Shell nên gọi wsl bằng lệnh Linux thích hợp tới bất kỳ đầu vào đường dẫn nào và chuyển bất kỳ đối số dòng lệnh nào được truyền cho hàm.

Vì mẫu này có thể được áp dụng cho bất kỳ lệnh nào nên chúng ta có thể trừu tượng hóa định nghĩa của các trình bao bọc này và tự động tạo chúng từ danh sách các lệnh cần nhập.

# 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 ' ')
    }
}
"@
}

Danh sách $command xác định các lệnh nhập. Sau đó, chúng tôi tự động tạo một trình bao bọc hàm cho từng hàm bằng cách sử dụng lệnh Invoke-Expression (trước tiên hãy xóa mọi bí danh có thể xung đột với hàm).

Hàm lặp lại các đối số dòng lệnh, xác định đường dẫn Windows bằng lệnh Split-Path и Test-Pathvà sau đó chuyển đổi các đường dẫn này thành đường dẫn WSL. Chúng tôi chạy các đường dẫn thông qua chức năng trợ giúp Format-WslArgument, mà chúng ta sẽ định nghĩa sau. Nó thoát khỏi các ký tự đặc biệt như dấu cách và dấu ngoặc đơn có thể bị hiểu sai.

Cuối cùng, chúng tôi truyền đạt wsl đầu vào đường ống và bất kỳ đối số dòng lệnh nào.

Với các trình bao bọc này, bạn có thể gọi các lệnh Linux yêu thích của mình theo cách tự nhiên hơn mà không cần thêm tiền tố wsl và không cần lo lắng về cách chuyển đổi đường dẫn:

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

Tập hợp lệnh cơ bản được hiển thị ở đây, nhưng bạn có thể tạo shell cho bất kỳ lệnh Linux nào bằng cách thêm nó vào danh sách. Nếu bạn thêm mã này vào hồ sơ PowerShell, các lệnh này sẽ có sẵn cho bạn trong mọi phiên PowerShell, giống như các lệnh gốc!

Thiết lập mặc định

Trong Linux, người ta thường xác định bí danh và/hoặc biến môi trường trong hồ sơ đăng nhập, đặt tham số mặc định cho các lệnh được sử dụng thường xuyên (ví dụ: alias ls=ls -AFh hoặc export LESS=-i). Một trong những nhược điểm của việc ủy ​​quyền thông qua Shell không tương tác wsl.exe - các cấu hình không được tải, do đó các tùy chọn này không có sẵn theo mặc định (tức là ls trong WSL và wsl ls sẽ hoạt động khác với bí danh được xác định ở trên).

PowerShell cung cấp $PSDefaultParameterValues, một cơ chế tiêu chuẩn để xác định các tham số mặc định, nhưng chỉ dành cho các lệnh ghép ngắn và các hàm nâng cao. Tất nhiên, chúng ta có thể tạo các hàm nâng cao từ shell của mình, nhưng điều này gây ra những phức tạp không cần thiết (ví dụ: PowerShell tương quan với các tên tham số một phần (ví dụ: -a tương quan với -ArgumentList), sẽ xung đột với các lệnh Linux lấy một phần tên làm đối số) và cú pháp xác định giá trị mặc định sẽ không phù hợp nhất (đối số mặc định yêu cầu tên của tham số trong key chứ không chỉ tên lệnh) .

Tuy nhiên, với một sửa đổi nhỏ đối với shell, chúng ta có thể triển khai một mô hình tương tự như $PSDefaultParameterValuesvà bật các tùy chọn mặc định cho các lệnh 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 ' ')
    }
}

Đi qua $WslDefaultParameterValues đến dòng lệnh, chúng tôi gửi tham số qua wsl.exe. Phần sau đây cho biết cách thêm hướng dẫn vào cấu hình PowerShell của bạn để định cấu hình cài đặt mặc định. Bây giờ chúng ta có thể làm được điều đó!

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

Vì các tham số được mô hình hóa sau $PSDefaultParameterValues, Bạn có thể thật dễ dàng để vô hiệu hóa chúng tạm thời bằng cách cài đặt key "Disabled" thành ý nghĩa $true. Một lợi ích bổ sung của bảng băm riêng biệt là khả năng vô hiệu hóa $WslDefaultParameterValues tách biệt với $PSDefaultParameterValues.

Hoàn thành đối số

PowerShell cho phép bạn đăng ký đoạn giới thiệu đối số bằng lệnh Register-ArgumentCompleter. Bash có sức mạnh công cụ tự động hoàn thành có thể lập trình. WSL cho phép bạn gọi bash từ PowerShell. Nếu chúng ta có thể đăng ký các phần hoàn thành đối số cho trình bao bọc hàm PowerShell của mình và gọi bash để tạo các phần hoàn thành, thì chúng ta sẽ có được phần hoàn thành đối số đầy đủ với độ chính xác tương tự như chính 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]
    }
}

Mã này hơi dày đặc nên không hiểu một số chức năng bên trong của bash, nhưng về cơ bản những gì chúng tôi làm là thế này:

  • Đăng ký trình hoàn thiện đối số cho tất cả các hàm bao hàm của chúng tôi bằng cách chuyển danh sách $commands trong tham số -CommandName cho Register-ArgumentCompleter.
  • Chúng tôi ánh xạ từng lệnh tới hàm shell mà bash sử dụng để tự động hoàn thành (để xác định các thông số kỹ thuật tự động hoàn thành, sử dụng bash $F, viết tắt của complete -F <FUNCTION>).
  • Chuyển đổi đối số PowerShell $wordToComplete, $commandAst и $cursorPosition sang định dạng mà các chức năng tự động hoàn thành của bash mong đợi theo thông số kỹ thuật lập trình tự động hoàn thành khà khà.
  • Chúng tôi soạn một dòng lệnh để chuyển sang wsl.exe, để đảm bảo rằng môi trường được thiết lập chính xác, gọi hàm tự động hoàn thành thích hợp và xuất kết quả theo kiểu từng dòng một.
  • Sau đó chúng tôi gọi wsl bằng dòng lệnh, chúng tôi phân tách đầu ra bằng dấu phân cách dòng và tạo cho mỗi dòng CompletionResults, sắp xếp chúng và thoát khỏi các ký tự như dấu cách và dấu ngoặc đơn có thể bị hiểu sai.

Do đó, các shell lệnh Linux của chúng tôi sẽ sử dụng tính năng tự động hoàn thành giống hệt như bash! Ví dụ:

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

Mỗi tự động hoàn thành cung cấp các giá trị cụ thể cho đối số trước đó, đọc dữ liệu cấu hình, chẳng hạn như các máy chủ đã biết từ WSL!

<TAB> sẽ chuyển qua các tham số. <Ctrl + пробел> sẽ hiển thị tất cả các tùy chọn có sẵn.

Ngoài ra, vì hiện tại chúng tôi có tính năng tự động hoàn thành bash nên bạn có thể tự động hoàn thành các đường dẫn Linux trực tiếp trong PowerShell!

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

Trong trường hợp quá trình tự động hoàn thành bash không tạo ra bất kỳ kết quả nào, PowerShell sẽ hoàn nguyên về đường dẫn Windows mặc định của hệ thống. Vì vậy, trong thực tế, bạn có thể sử dụng đồng thời cả hai con đường theo ý mình.

Kết luận

Sử dụng PowerShell và WSL, chúng ta có thể tích hợp các lệnh Linux vào Windows dưới dạng ứng dụng gốc. Không cần phải tìm kiếm các bản dựng Win32 hoặc tiện ích Linux hay làm gián đoạn quy trình làm việc của bạn bằng cách truy cập trình bao Linux. Chỉ cài đặt WSL, cấu hình Hồ sơ PowerShell и liệt kê các lệnh bạn muốn nhập! Tự động hoàn thành phong phú cho các tham số lệnh và đường dẫn tệp của Linux và Windows là chức năng thậm chí không có sẵn trong các lệnh Windows gốc hiện nay.

Mã nguồn đầy đủ được mô tả ở trên cũng như các hướng dẫn bổ sung để kết hợp nó vào quy trình làm việc của bạn đều có sẵn đây.

Lệnh Linux nào bạn thấy hữu ích nhất? Những điều phổ biến nào khác bị thiếu khi làm việc trong Windows? Viết trong phần bình luận hoặc trên GitHub!

Nguồn: www.habr.com

Thêm một lời nhận xét