試験運用中なLinux備忘録・旧記事

はてなダイアリーで公開していた2007年5月-2015年3月の記事を保存しています。

任意の環境変数をGUIで変更して外部プロセスを起動するツールを作成

環境変数は端末シェルのbashやzshではexport、tcshではsetenvというコマンドでスクリプトから設定できるが、デスクトップ環境などでGUIを用いて環境変数を設定することはできないため、GUI上で環境変数を一覧に登録してアプリケーション下部の入力欄に入力したコマンド行を実行できるツールを作成してみた。

動作にはPyGTK(バージョン2.6以上)とPyGObjectのパッケージが必要。
[任意]ファイル名: setenvexecpygtk.py

#! /usr/bin/python
# -*- encoding: utf-8 -*-

import shlex
import sys
import os
try:
  import pygtk
  pygtk.require('2.0')
except:
  pass
try:
  import gtk
except:
  print >> sys.stderr, 'Error: PyGTK is not installed'
  sys.exit(1)
try:
  from glib import spawn_async as glib_spawn_async
  from glib import SPAWN_SEARCH_PATH as glib_SPAWN_SEARCH_PATH
  from glib import GError as glib_GError
except:
  try:
    from gobject import spawn_async as glib_spawn_async
    from gobject import SPAWN_SEARCH_PATH as glib_SPAWN_SEARCH_PATH
    from gobject import GError as glib_GError
  except:
    print >> sys.stderr, 'Error: cannot import GLib functions'
    sys.exit(1)


class TreeViewWithColumn(gtk.TreeView):
  """
  コラムを含んだツリービュー
  """
  (
    COLUMN_NAME,
    COLUMN_VALUE,
    COLUMN_ORIG,
  ) = range(3)
  def __init__(self, *args, **kwargs):
    gtk.TreeView.__init__(self, *args, **kwargs)
    # セルのレンダラ
    self.__renderer_name = gtk.CellRendererText()
    self.__renderer_name.connect('edited', self.__on_text_edited, self.COLUMN_NAME)  # 最後の引数はユーザデータ
    self.__renderer_name.set_property('editable', True)
    self.__renderer_value = gtk.CellRendererText()
    self.__renderer_value.connect('edited', self.__on_text_edited, self.COLUMN_VALUE)
    self.__renderer_value.set_property('editable', True)
    # コラムの設定
    self.__col_name = gtk.TreeViewColumn('Name',
                                         self.__renderer_name,
                                         text=self.COLUMN_NAME)
    self.__col_name.set_resizable(True)
    self.__col_name.set_min_width(100)
    self.__col_value = gtk.TreeViewColumn('Value',
                                          self.__renderer_value,
                                          text=self.COLUMN_VALUE)
    self.__col_value.set_resizable(True)
    # コラムを追加
    self.append_column(self.__col_name)
    self.append_column(self.__col_value)
    # 複数行選択を可能にする
    self.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
  def __on_text_edited(self, widget, path, new_text, col):
    """
    テキスト用セルが編集されたときの処理
    col(ユーザデータ)はコラム番号
    """
    # get_model()でツリービューに関連付けられたデータを取り出して
    # 引数の情報とあわせてデータの値を変更する
    model = self.get_model()
    model.set_value(model.get_iter(path), col, new_text)

class MainWindow(gtk.Window):
  """
  メインウィンドウ
  """
  def __init__(self, *args, **kwargs):
    gtk.Window.__init__(self, *args, **kwargs)
    # ショートカットキー(アクセラレータ)
    self.__accelgroup = gtk.AccelGroup()
    self.add_accel_group(self.__accelgroup)
    # メニュー項目
    self.__item_quit = gtk.ImageMenuItem(gtk.STOCK_QUIT, self.__accelgroup)
    self.__menu_file = gtk.Menu()
    self.__menu_file.add(self.__item_quit)
    self.__item_file = gtk.MenuItem('_File')
    self.__item_file.set_submenu(self.__menu_file)
    self.__menubar = gtk.MenuBar()
    self.__menubar.append(self.__item_file)
    # ツリービュー
    self.__treeview = TreeViewWithColumn(model=gtk.ListStore(str, str, str))
    self.__treeview.set_rules_hint(True)
    # ツリービュー向けスクロールウィンドウ
    self.__sw = gtk.ScrolledWindow()
    self.__sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
    self.__sw.add(self.__treeview)
    # 実行ボタン
    self.__btn_exec = gtk.Button(stock=gtk.STOCK_EXECUTE)
    # 追加/削除/クリアボタン
    self.__btn_add = gtk.Button(stock=gtk.STOCK_ADD)
    self.__btn_del = gtk.Button(stock=gtk.STOCK_DELETE)
    self.__btn_clear = gtk.Button(stock=gtk.STOCK_CLEAR)
    # 名前/値/コマンド行のテキスト入力欄(エントリ)
    self.__entry_name = gtk.Entry()
    self.__entry_value = gtk.Entry()
    self.__entry_cmdline = gtk.Entry()
    # ラベル
    self.__label_name = gtk.Label()
    self.__label_name.set_text_with_mnemonic('_Name:')
    self.__label_name.set_mnemonic_widget(self.__entry_name)
    self.__label_value = gtk.Label()
    self.__label_value.set_text_with_mnemonic('_Value:')
    self.__label_value.set_mnemonic_widget(self.__entry_value)
    # レイアウト用コンテナ
    self.__table_add = gtk.Table(2, 2)
    self.__table_add.attach(self.__label_name, 0, 1, 0, 1)   # X:0-1 Y:0-1 左上
    self.__table_add.attach(self.__entry_name, 1, 2, 0, 1)   # X:1-2 Y:0-1 右上
    self.__table_add.attach(self.__label_value, 0, 1, 1, 2)  # X:0-1 Y:1-2 左下
    self.__table_add.attach(self.__entry_value, 1, 2, 1, 2)  # X:1-2 Y:1-2 右下
    self.__vbox_btn = gtk.VBox()      # 削除/クリアボタンを縦に並べる
    self.__vbox_btn.pack_start(self.__btn_del, expand=False)
    self.__vbox_btn.pack_start(self.__btn_clear, expand=False)
    self.__hbox_add = gtk.HBox()
    self.__hbox_add.pack_start(self.__table_add)
    self.__hbox_add.pack_start(self.__btn_add, expand=False, fill=False)
    self.__hbox_tv = gtk.HBox()       # ツリービューとボタン群を横に
    self.__hbox_tv.pack_start(self.__sw)
    self.__hbox_tv.pack_start(self.__vbox_btn, expand=False, fill=False)
    self.__hbox_cmdline = gtk.HBox()  # コマンド行と実行ボタンを横に
    self.__hbox_cmdline.pack_start(self.__entry_cmdline)
    self.__hbox_cmdline.pack_start(self.__btn_exec, expand=False, fill=False)
    self.__vbox = gtk.VBox()
    self.__vbox.pack_start(self.__menubar, expand=False, fill=False)
    self.__vbox.pack_start(self.__hbox_add, expand=False, fill=False)
    self.__vbox.pack_start(self.__hbox_tv)
    self.__vbox.pack_start(self.__hbox_cmdline, expand=False, fill=False)
    # シグナル
    self.connect('delete_event', gtk.main_quit)
    self.__item_quit.connect('activate', gtk.main_quit)
    self.__btn_add.connect('clicked', self.__on_btn_add_clicked)
    self.__btn_del.connect('clicked', self.__on_btn_del_clicked)
    self.__btn_clear.connect('clicked', self.__on_btn_clear_clicked)
    self.__btn_exec.connect('clicked', self.__on_btn_exec_clicked)
    self.__entry_cmdline.connect('activate', self.__on_btn_exec_clicked)
    # ウィンドウ
    self.add(self.__vbox)
    self.set_size_request(350, 300)
    self.set_title('SetEnvExecPyGTK')
    # コマンド行部分にフォーカス
    self.__entry_cmdline.grab_focus()
  def __on_btn_add_clicked(self, widget):
    """
    追加ボタンが押されたときの処理
    モデルに名前と値のテキストエントリの値を追加
    """
    name = self.__entry_name.get_text()
    value = self.__entry_value.get_text()
    # 名前が空の場合は受け付けないことにする(値が空文字列はOK)
    if name == '':
      return
    try:
      orig = os.environ[name]
    except:
      orig = None
    # ListStore/TreeModelへデータ追加
    self.__treeview.get_model().append((name, value, orig))  # タプルで追加
    self.__entry_name.set_text('')
    self.__entry_value.set_text('')
  def __on_btn_del_clicked(self, widget):
    """
    削除ボタンが押されたときの処理
    選択された複数の項目をモデルから削除
    """
    (model, selected) = self.__treeview.get_selection().get_selected_rows()
    iters = [model.get_iter(path) for path in selected]
    for iter in iters:
      (name, value, orig) = model.get(iter, self.__treeview.COLUMN_NAME, self.__treeview.COLUMN_VALUE, self.__treeview.COLUMN_ORIG)
      # 元の値に戻す
      if orig:
        os.environ[name] = orig
      else:
        # 環境変数を消す場合はdel文で(Noneの値を指定してもダメ)
        del os.environ[name]
      # 消す
      model.remove(iter)
  def __on_btn_clear_clicked(self, widget):
    """
    クリアボタンが押されたときの処理
    モデルのデータを全て消す
    clear()を用いず、消しながら環境変数を戻している
    """
    model = self.__treeview.get_model()
    iter = model.get_iter_first()
    if iter:  # 空の場合の対策
      while True:
        (name, value, orig) = model.get(iter, self.__treeview.COLUMN_NAME, self.__treeview.COLUMN_VALUE, self.__treeview.COLUMN_ORIG)
        # 戻す
        if orig:
          os.environ[name] = orig
        else:
          del os.environ[name]
        # 消す
        if model.remove(iter) == False:  # PyGTK 2.4未満では挙動が異なる
          break
  def __on_btn_exec_clicked(self, widget):
    """
    実行ボタンが押されたときの処理
    """
    # 環境変数の設定
    model = self.__treeview.get_model()
    iter = model.get_iter_first()
    # ツリービューの各データを読み込んで環境変数にセット
    while iter:
      (name, value, orig) = model.get(iter, self.__treeview.COLUMN_NAME, self.__treeview.COLUMN_VALUE, self.__treeview.COLUMN_ORIG)
      os.environ[name] = value
      iter = model.iter_next(iter)
    # 非同期でコマンドを実行
    # glib.SPAWN_SEARCH_PATHによりPATHから実行ファイルの探索を行う
    # 失敗するとglib.GError例外が発生
    try:
      glib_spawn_async(shlex.split(self.__entry_cmdline.get_text()), flags=glib_SPAWN_SEARCH_PATH)
    except glib_GError, (msg):
      errdlg = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, buttons=gtk.BUTTONS_CLOSE, message_format=str(msg))
      errdlg.set_title('Spawn failed')
      errdlg.run()
      errdlg.destroy()
    # コマンド行の入力欄をを空にする
    self.__entry_cmdline.set_text('')

class SetEnvExecPyGTK:
  """
  環境変数をセットしてコマンドを実行するGUIツール
  """
  def main(self):
    """
    アプリケーションのメイン処理
    """
    win = MainWindow()
    win.show_all()
    gtk.main()


if __name__ == '__main__':
  app = SetEnvExecPyGTK()
  app.main()

メモ

  • 一覧に追加された項目をコマンド実行前に環境変数に設定し、項目のリストからの削除時にその環境変数を削除するのだが、DISPLAYなど、元々定義されていた環境変数が消えてしまうのは問題なので、リストの非表示項目として「追加前のその環境変数の値」を設け、元々未定義だったものはリストから消されるときに環境変数をそのまま削除するが、元々値の定義されていたものは元の値を復元するようにしている
  • 外部コマンドの起動にはPyGObjectのglib.spawn_async()*1を使用しているが、この関数はコマンド行をリストで受け付けるため、テキスト入力欄の文字列を分割するのに「コマンド行のような文字列を解析してリストに格納する上でのメモ(後半)」のshlex.split()を用いている*2
  • Python内で環境変数を設定するにはos.environへの代入でよいが、未定義にするにはdel文を用いる

関連記事:

関連URL:

使用したバージョン:

  • PyGObject 2.20.0
  • PyGTK 2.16.0

*1:GLibの外部プロセス非同期実行関数をPythonから利用するもので、Vala言語で以前「Vala言語で外部プロセスを実行する(簡単な例・メモ)」からの一連の記事で扱っている

*2:リストへの分割の処理は一度自前で書いていたが、shlex.split()が使えることが分かってからこちらに変更した