Ενσωμάτωση εντολών 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). Ένα από τα μειονεκτήματα του proxy μέσω ενός μη διαδραστικού κελύφους wsl.exe - ότι τα προφίλ δεν έχουν φορτωθεί, επομένως αυτές οι επιλογές δεν είναι διαθέσιμες από προεπιλογή (δηλ. ls στο WSL και wsl ls θα συμπεριφέρεται διαφορετικά με το ψευδώνυμο που ορίζεται παραπάνω).

Το PowerShell παρέχει $PSDefaultParameterValues, ένας τυπικός μηχανισμός για τον καθορισμό προεπιλεγμένων παραμέτρων, αλλά μόνο για cmdlet και προηγμένες λειτουργίες. Φυσικά, μπορούμε να δημιουργήσουμε προηγμένες συναρτήσεις από τα κελύφη μας, αλλά αυτό εισάγει περιττές επιπλοκές (για παράδειγμα, το 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 σάς επιτρέπει να καλείτε το 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. Μόλις εγκαταστήστε το WSL, Διαμορφώστε Προφίλ PowerShell и απαριθμήστε τις εντολές που θέλετε να εισαγάγετε! Η πλούσια αυτόματη συμπλήρωση για παραμέτρους εντολών και διαδρομές αρχείων Linux και Windows είναι λειτουργικότητα που δεν είναι ακόμη διαθέσιμη στις εγγενείς εντολές των Windows σήμερα.

Ο πλήρης πηγαίος κώδικας που περιγράφεται παραπάνω, καθώς και πρόσθετες οδηγίες για την ενσωμάτωσή του στη ροή εργασίας σας, είναι διαθέσιμος εδώ.

Ποιες εντολές Linux θεωρείτε πιο χρήσιμες; Ποια άλλα κοινά πράγματα λείπουν όταν εργάζεστε στα Windows; Γράψτε στα σχόλια ή στο GitHub!

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο