Konsola: sprytne przeklejanie argumentów

Wpisujesz komendę w konsoli, uruchamiasz… i coś jest nie tak. Trzeba poprzednie polecenie zmodyfikować i odpalić ponownie. Co teraz? Wpisywanie wszystkiego od zera nie wchodzi w grę. Można pomóc sobie myszą, zaznaczać, kopiować, wklejać, dusić backspace – są jednak szybsze opcje, niewymagające odrywania rąk od klawiatury. Opowiem o nich, zaczynając od tych wspominanych w większości poradników (jak !! i !$), kończąc na rzeczach mniej znanych: odwoływaniu się do argumentów po indeksie i usuwaniu z nich rozszerzeń i fragmentów ścieżki. Opisywane mechanizmy działają w Zsh i Bashu. Część może uprościć pisanie skryptów.

Absolutne podstawy

Załóżmy, że wykonaliśmy jakieś polecenie i chcemy coś poprawić:

% cp result /tmp 
cp: -r not specified; omitting directory 'result'

Tu przeoczyliśmy, że result to katalog i polecenie się nie powiodło. Trzeba spróbować raz jeszcze, tym razem dodając potrzebny przełącznik -r, czyli: cp -r result /tmp. Jak?

  • na klawiaturze: – przywołuje ostatnie polecenie
  • na klawiaturze: Ctrl+← – przesuwa kursor o słowo w lewo
    • to bashowy skrót, w Zsh domyślnie jest inaczej: Alt+b słowo w lewo (backward), Alt+f słowo w prawo (forward); ja przemapowałem sobie na strzałki, żeby nawigacja działała tak, jak w IntelliJ czy LibreOffice Write
    • u biednych użytkowników Maca nie działa ani pierwszy, ani drugi sposób, ani systemowy skrót na przechodzenie po słowach, a na StackOverflow jest dużo przeczących sobie odpowiedzi – ja umywam ręce

Wciskamy raz klawisz w górę, potem 2 razy drugi skrót, mamy kursor na początku result, piszemy -r, enter i gotowe.

Inna sytuacja: pomyłka w pierwszym argumencie (miało być result, wpisaliśmy results).

% cp results ../../src/test/resources/com/example/expected
cp: cannot stat 'results': No such file or directory

Nie trzeba ganiać kursorem. Można wpisać:

% cp result !$

co zostanie przetłumaczone na:

cp result ../../src/test/resources/com/example/expected

Albo: polecenie jest dobre, ale zabrakło sudo na początku:

% apt install make gcc
 E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)
 E: Unable to acquire the dpkg frontend lock (/var/lib/dpkg/lock-frontend), are you root?

Można wpisać:

sudo !!

a będzie wykonane:

sudo apt install make gcc

Jeśli apt nie był ostatnim poleceniem, można go przywołać w ten sposób:

sudo !apt

Ja bardzo często używam !$ przy tworzeniu nowych projektów:

cd ~/devel/bugs
mkdir spring-data-id-accessor
cd !$
gradle init

Podsumowując:

  • !$ jest zamieniane na ostatni argument poprzedniego polecenia
  • !! jest zamieniane na całe ostatnie polecenie
  • !apt jest zamieniane na ostatnie polecenie z historii, które zaczyna się od „apt”

Analogicznie:

% echo raz dwa trzy
raz dwa trzy
% echo !^ 
raz
% echo raz dwa trzy
raz dwa trzy
% rm !* 
rm: cannot remove 'raz': No such file or directory 
rm: cannot remove 'dwa': No such file or directory 
rm: cannot remove 'trzy': No such file or directory
  • !^ to pierwszy argument poprzedniego polecenia (jak w wyrażeniach regularnych: ^ to początek a $ to koniec)
  • !* to wszystkie argumenty (analogicznie do $* w skryptach)

Zaawansowany dostęp do argumentów

Tym razem skupmy się na jednym przykładzie: mamy plik i chcemy go rozpakować.

unzip -q ci-job-4321-tests.zip -d /tmp/ci-job-4321-tests

Jak zrobić, żeby się nie orobić? Zaprezentuję kilka podejść.

Ręczne wpisywanie i tabulator

Możemy tak:

unzip -q ci-<tab> -d /tmp/ci<enter>

Problem: rozpakowujemy do /tmp/ci/ niezależnie od tego, jak nazywał się oryginalny plik. Tak się nie da, gdy mamy dużo zipów i wszystkie rozpakowujemy – przemieszają się, a chcemy, żeby każdy dostał się w inne miejsce.

Kopiowanie całości myszą

Klikając dwukrotnie gdziekolwiek wewnątrz „wyrazu”, zaznaczamy go. Wtedy wciśnięcie środkowego klawisza myszy wklei go na pozycję kursora.

wklejanie tekstu w konsoli środkowym klawiszem myszy

Wada: wkleiliśmy z rozszerzeniem. Albo trzeba jeszcze skasować rozszerzenie, albo zgadzamy się na katalog z rozszerzeniem .zip. Plus trzeba oderwać ręce od klawiatury i machać myszką.

Kopiowanie części myszą

Jak wyżej, tylko zamiast zaznaczać dwukrotnym kliknięciem, normalnie ciągniemy zaznaczenie. W ten sposób możemy zaznaczyć bez rozszerzenia. W praktyce sposób zawodny: łatwo spiesząc się zaznaczyć o 1 znak za dużo lub za mało.

Kopiowanie całości bez myszy

To samo, ale bez myszy:

  • Ctrl+w kasuje poprzednie słowo
  • Ctrl+y wkleja („yank”)
  • Alt+Backspace kasuje do slasha lub kropki (w Bashu)
ctrl+w wycina, ctrl+y wkleja
Ctrl+w kasuje poprzednie słowo

U mnie w Zsh podpiąłem sobie pod Alt+Backspace zachowanie podobne jak w Bashu, ale inaczej zdefiniowałem granicę: jako slash i znak równości. W ten sposób jeśli mam opcję --configuration=compileClasspath to skrót wytnie mi wartość opcji, a nie całą opcję. Kod inspirowany rozwiązaniem ze StrackOverflow:

backward-kill-dir () {
    local WORDCHARS=${WORDCHARS/=\/}
    zle backward-kill-word
}
zle -N backward-kill-dir
bindkey '^[^?' backward-kill-dir

Kopiowanie części bez myszy

W Zsh działają jeszcze takie skróty:

  • Ctrl+spacja zaczyna zaznaczanie tekstu
  • wtedy i rozszerzają zaznaczenie
  • Esc+w kopiuje zaznaczony tekst
  • Ctrl+y wkleja zaznaczenie (jak poprzednio)
ctrl+spacja zaczyna zaznaczenie, esc+w kopiuje
Ctrl+spacja umożliwia zaznaczanie i kopiowanie tekstu wyłącznie klawiaturą

Nieintuicyjna rzecz: zaznaczenie trzeba „przeciągnąć” o 1 znak za daleko, aż do kropki – kropka nie zostanie skopiowana.

W praktyce jest to tak niewygodne i wolne, że jak dla mnie zostaje tylko ciekawostką.

Odwołanie się do argumentu

unzip -q ci-<tab> -d /tmp/!#:2<enter>

zostanie rozwinięte do:

unzip -q ci-job-4321-tests.zip -d /tmp/ci-job-4321-tests.zip
  • !# to aktualna linia poleceń
  • !#:2 to drugi argument z aktualnej linii poleceń

Jest lepiej, bo nie trzeba odrywać rąk od klawiatury. Gorzej, że znowu kopiujemy niechciane rozszerzenie.

Manipulacja argumentem

unzip -q ci-<tab> -d /tmp/!#:2:r<enter>

zostanie rozwinięte do:

unzip -q ci-job-4321-tests.zip -d /tmp/ci-job-4321-tests
  • :r usuwa rozszerzenie zostawiając tylko root name

Więcej o dostępie do argumentów

Już widzę wątpliwości: po co mam pisać, skoro świetnie władam myszką. Moja odpowiedź:

  1. nazwy niewygodne do zaznaczania: za długie do ciągnięcia po nich myszką, wypadające na łamaniu linii, ze specjalnymi znakami, które mylą ludzkie oko albo psują zaznaczanie podwójnym kliknięciem
    • podwójne klikanie w konsoli na Pl-Warszawa.oga zaznacza całą nazwę pliku, ale przy Zh-华沙.oga zaznacza tylko 1 znak
  2. skrypty

Modyfikator :r działa na każdą zmienną, nie tylko na argumenty. Może być bardzo przydatny w skryptach:

for i (*un*.jpg); do convert $i -resize 50% ${i:r}-small.jpg; done

${i:r} wypisuje zmienną i z wyciętym rozszerzeniem. W ten sposób z pliku sun.jpg dostaniemy zmniejszoną wersję w sun-small.jpg.

Jeszcze jeden przykład.

mv ../../01/in.txt !#^:h/out.txt

daje

mv ../../01/in.txt ../../01/out.txt
  • !#^ to pierwszy argument z aktualnej linii (to samo, co !#:^, ale przy ^ nie trzeba dwukropka)
  • :h usuwa 1 poziom katalogu („head”)

Ściągawka

Ogólna składnia to:

event:word:modifier

  1. event to oznaczenie, co wybrać z historii poleceń
    1. !! poprzednie polecenie
    2. !# aktualna linia
    3. !-2 2 polecenia temu
  2. word to które słowa wziąć z wybranej wcześniej linii
    1. ^ pierwszy argument, nie trzeba poprzedzić dwukropkiem
    2. $ ostatni argument, nie trzeba poprzedzić dwukropkiem
    3. 0 pierwsze słowo, czyli sama komenda
    4. 8 ósmy argument, czyli dziewiąte słowo
  3. modifier to operacja na wybranym wcześniej słowie
    1. r usuwa rozszerzenie (root name)
    2. h usuwa poziom katalogu (head)

Części :word i :modifier są opcjonalne.

Powtarzanie z modyfikacją

rm -r src/main/java/com/example/compat
^main^test^

daje w wyniku drugiego polecenia

rm -r src/test/java/com/example/compat

Zastępowane jest tylko pierwsze wystąpienie. Żeby zastąpić wszystkie wystąpienia, trzeba dodać modyfikator :G: ^main^test^:G.

Dalsza lektura

To nie wszystkie możliwości, ale myślę, że zapamiętanie powyższych to i tak spory wysiłek. Dla ciekawych polecam pełną dokumentację Zsh o Expansion. Nie jest to najłatwiejszy w czytaniu tekst, ale z przykładami z tego artykułu powinno iść łatwiej. Nie trzeba uczyć się wszystkiego na pamięć – można sięgnąć do dokumentacji przy pisaniu skryptów.

Jeśli pominąłem jakąś z podstawowych sztuczek – zapraszam do komentowania.