Ez is egy megoldás

Van az úgy néha, hogy az eval mégsem annyira evil

A Sublime az a szerkesztő, amiből szinte mindig fut egy példány a gépemen. Régen kizárólag ebben írtam kódot, manapság jórészt csak néhány untitled fájl van benne kusza jegyzetekkel, kódrészekkel, félig megírt üzenetekkel, ami éppen jön.

Főleg szövegek átalakítására használom, ha mondjuk van egy pár sornyi szöveg, aminek minden sorát idézőjelbe kellene tenni, vagy néhány sornyi adatot ábécé sorrendbe kell rendezni, aztán a sorokból egy hosszú vesszővel elválasztott sort kell csinálni, ilyesmik. Még az éppen aktuálisan kód írásra használt IDE-ből is hajlamos vagyok blokkokat kimásolni, Sublime-ban átalakítani és visszamásolni.

Persze lehet mondani, hogy a Vim (vagy az Emacs) ezt sokkal hatékonyabban és gyorsabban csinálja, meg hát mennyi minden másra is jó. Nekem viszont ez az egeres/multi-kurzoros világ jobban fekszik, mint a Vim-varázslat. Tudom, fura vagyok.

Néha viszont előkerülnek olyan problémák, amikkel szemben kevésnek bizonyul az eszköz. Olyasmikre gondolok, aminél már ott motoszkál az agyad egy hátsó szegletében, hogy lehet, hogy erre össze kéne dobni egy kis programocskát, de legalábbis nyitni egy REPL-t.

Nézzünk egy egyszerű példát. Mondjuk számokat szeretnénk kigenerálni 1-től 10-ig, mindegyiket új sorba. Persze az ügyeletes Vim-mágus rögtön mondaná, hogy csak egy gyors i 1 Esc q a Y p Ctrl+a q 8 @ a és kész is, de ezt valahogy nem érzem annyira életszerűnek.[1]

Szerencsére a Sublime is támogatja a plugin-eket, még nagyobb szerencse, hogy egy normális nyelvet választottak. Nincs is annál rosszabb, ha tetszik a program, szeretnél plugin-eket írni hozzá, de kiderül, hogy Perl-ben kellene.[2]

A kijelölés futtatása

Az első ötlet az volt, hogy lehetne mondjuk egy olyan parancsot csinálni, ami a kijelölt szöveget Python kódként lefuttatja és az eredményt visszaírja a kód helyére:[3]

eval.py
import sublime_plugin

class EvalSelectionsCommand(sublime_plugin.TextCommand):
  def run(self, edit):
    for region in self.view.sel():
      if region.empty():
        continue

      code = self.view.substr(region)
      result = str(eval(code))
      self.view.replace(edit, region, result)

Érdekes módon ez volt az első alkalom, hogy Python-ban eval()-t kellett használnom. Hamarosan azt is megtudtam, hogy mindannyiunkat becsaptak, egy másik eval() is készült... amit exec()-nek hívnak. Igen Drágaszág, az elsővel csak kifejezéseket lehet kiértékelni, a második az, amivel mindenféle Python kódot tudnak futtatni a kisz hobbitockák. De ne szaladjunk ennyire előre, maradjunk ennél a változatnál, amit még valahogy meg is kell futtatni.

eval.sublime-command
[
  { "caption": "Run selection", "command": "eval_selections" }
]

Így már ki tudjuk jelölni mondjuk a '\n'.join(map(str, range(1, 11))) szöveget, nyomhatunk egy Ctrl+Shift+p-t, kikeressük a parancsot és kész is. Valahogy nem érződik annyira kényelmesnek, mint amire számítottam, az viszont biztos, hogy egyszerűségéhez képest sok mindenre jó lehet még.

A kijelölések feldolgozása

Nézzünk meg azért egy másik megközelítést is. Lehetne mondjuk valami olyasmi, hogy nem a kijelölésben van a kód, hanem a kód a kijelölésekkel dolgozik.

eval.py
import sublime_plugin

class EvalSelectionsCommand(sublime_plugin.TextCommand):
  def run(self, edit, text):
    for idx, region in enumerate(self.view.sel()):
      data = self.view.substr(region)
      result = str(eval(text, {'d': data, 'i': idx}, {}))
      self.view.replace(edit, region, result)

  def input(self, args):
    return sublime_plugin.TextInputHandler()

Kicsit bonyolódik a helyzet az eval() körül. Átadunk neki pár változót, amit aztán a kifejezésben tudunk felhasználni. Így csinálhatjuk például azt, hogy van 10 üres sorunk, mindegyikben egy kijelölés, megfuttatjuk a parancsot és beírjuk kódnak azt, hogy i+1. Vagy ha mondjuk van a kijelölésekben már valami adat, akkor írhatnánk azt is, hogy f'{i+1}. {d}'.

Az ötlet egy későbbi változata, ahol a d változó már át lett nevezve _-ra és került bele egy előnézet is az első kijelölés alapján.

Okosabb futtatás

Ugorjunk egy kicsit még vissza az exec()-hez. Mondjuk van az alábbi egyszerű kis kódunk, amit kijelölés után meg szeretnénk futtatni:

for i in range(10):
  print(i+1)

Erre az első változatunk azt fogja mondani, hogy SyntaxError: invalid syntax, úgyhogy okosítsuk fel egy kicsit:

eval.py
import sublime_plugin
import traceback

from io import StringIO
from contextlib import redirect_stdout

class EvalSelectionsCommand(sublime_plugin.TextCommand):
  def run(self, edit):
    for region in self.view.sel():
      if region.empty():
        continue

      code = self.view.substr(region)
      self.view.replace(edit, region, self.__run_code(code))

  def __run_code(self, code):
    try:
      return str(eval(code))
    except Exception as e:
      try:
        return self.__exec_code(code)
      except Exception as e:
        return ''.join(traceback.format_exception(type(e), e, e.__traceback__))

  def __exec_code(self, code):
    f = StringIO()
    with redirect_stdout(f):
      exec(code)
    return f.getvalue()

Először megpróbáljuk lefuttatni eval()-lal, aztán ha nem megy, jöhet az exec(). Kicsit trükközni kell, mert a print() utasítás (meg minden más) kimenete a Sublime konzoljában kötne ki, ezért átirányítjuk egy változóba.

Bónuszként belekerült egy kis hibakezelés is, hogy ne a konzolban kelljen utánanézni, ha valami baj van a kóddal, amit futtatni szeretnénk. Így már megkapjuk a kódunk helyére a hibát (természetesen egy Ctrl+z visszahozza a kódot).

A lehetőségek tárháza

Rengeteg irány van, amerre tovább lehet vinni ezt a kis plugin-kezdeményt. Csak néhány ötlet a teljesség igénye nélkül:

  • a két változat lehet külön-külön parancs is, más eszközök más feladatokra
  • gyakran használt dolgokra csinálhatunk segédfüggvényeket, amit hozzáadhatunk az eval()-hoz/exec()-hez, hogy lehessen őket használni a kódon belül
  • a kód beíró részénél lehet megjeleníteni előnézetet, hogy hogyan fog kinézni a futtatás után a tartalom
  • a kimenetet meg lehetne jeleníteni akár egy külön ablakban is

Ám adásidőnk végéhez közeledünk, így a téma további boncolgatását kénytelen vagyok a kedves olvasóra bízni.


Jegyzetek

1. Létezik egyszerűbb/rövidebb megoldás is, de számomra ez érződött a leginkább Vim-jellegűnek. További részletek itt.

2. Csak egy gyerekkori trauma, ne is törődjetek vele. Régen nagyon is tetszett az Irssi IRC kliens, de az agyam nem volt hajlandó befogadni a Perl-t, hogy scripteket tudjak hozzá írni.

3. Természetesen léteznek erre már kész plugin-ek is, de abban úgy már nincsen semmi móka.

This post is also available in english: This could also work

Hozzáfűznél valamit?

Dobj egy emailt a blog kukac deadlime pont hu címre.

Feliratkoznál?

Az RSS feed-et ajánljuk, ha kedveled a régi jó dolgokat.