Mayaスクリプト(13):スクリプト実行ツール "StmScriptLauncher"
こんにちは。
今回は「Mayaでスクリプトを使うためのPythonスクリプト」を紹介します。
このスクリプトについての解説記事をnoteにアップしました。(約20000字)
ご興味あれば是非ご覧下さい。
スクリプト概要
機能
- 指定したフォルダ下のスクリプトの一覧表示
- UIからのスクリプトの実行
- スクリプトのプレビュー
用途
- シェルフを利用しないスクリプトへのアクセス
操作・UI
Ul
UI:クリックで拡大
操作
スクリプトを実行してUIを開きます。
- リストからスクリプトを選択します。
- 検索窓からのフィルター検索が可能です。
- ヒストリーからも同様に選択が可能です。
- 選択したスクリプトは右側にプレビュー表示されます。
- 各種ボタンからリストで選択したスクリプトを実行します。
- Source:
スクリプト内部に実行文が書かれている場合はこちらを使用します。 - Call:
スクリプト名で実行します。 ファイル名と異なる名前のコマンドを呼び出したい場合はボタンの上にある欄にコマンド名を入力して実行します。 - Auto Execute:
スクリプト内部の実行文の有無に応じて自動でSource / Call を切り替えます。
※リスト項目のダブルクリックでもこのボタンと同じ処理を行います。
- Source:
Auto Execute時の優先度
Auto Execute時の判別における優先度は
- 内部に実行コマンド名(スクリプト名または入力名)と同じ名称のプロシージャ・関数名が見つかればそれを実行
- 見つからなければSourceを実行
という順番です。
そのため、内部に実行文がありそれを実行したいものの、スクリプト名と同じ名前の異なる関数・プロシージャが存在するといった場合は、その名前を入力して実行する必要があります。
スクリプトの保存先
スクリプトが参照するフォルダは以下の4つです。
- ドキュメント > maya > scripts
- ドキュメント > maya > (バージョン) > (言語) > scripts
- ドキュメント > maya > (バージョン) > scripts
- Mayaインストールディレクトリ(バージョン別) > scripts
これらのフォルダの下にあるスクリプトを取得します。
ヒストリー
直近で使用したスクリプトを保存し、ヒストリー欄に表示します。
コード全文
ファイルとして保存して呼び出す必要のないように、ここにコード全文を載せてしまいます。
スクリプトエディタにそのまま貼り付けて実行可能です。
#=== [StmScriptLauncher] ===
# Function: A script for previewing and executing other scripts.
# Last Update: 2026.04
# To report errors, please contact me via my website: [Steem's CG Developin'].
# ( link: https://steems-cg-developin.com/ )
#==========================================
import maya.cmds as cmds
import maya.mel as mel
import os
import glob
import re
import ast
import importlib
import sys
class StmScriptLauncher:
def __init__(self):
self.window_id = "scriptLauncherWin"
self.history_var = "scriptLauncher_history"
self.script_data = {}
version = cmds.about(v=True)
lang = cmds.about(uiLanguage=True)
user_docs = cmds.internalVar(userAppDir=True)
maya_install_dir = os.environ.get('MAYA_LOCATION')
search_paths = [
os.path.join(user_docs, "scripts"),
os.path.join(user_docs, version, lang, "scripts"),
os.path.join(user_docs, version, "scripts"),
os.path.join(maya_install_dir, "scripts")
]
self.collect_scripts(search_paths)
self.create_ui()
def collect_scripts(self, paths):
for path in paths:
if not os.path.exists(path): continue
files = glob.glob(os.path.join(path, "*.mel")) + glob.glob(os.path.join(path, "*.py"))
for f in files:
file_name = os.path.basename(f)
if file_name not in self.script_data:
self.script_data[file_name] = f.replace("\\", "/")
def create_ui(self):
if cmds.window(self.window_id, exists=True):
cmds.deleteUI(self.window_id)
self.win = cmds.window(self.window_id, title="StmScriptLauncher", widthHeight=(800, 600))
pane = cmds.paneLayout(configuration='vertical2', separatorThickness=5)
left_col = cmds.columnLayout(adjustableColumn=True, rowSpacing=5, columnOffset=("both", 5))
cmds.text(label="Search (AND):", align="left", height=20)
self.search_field = cmds.textField(textChangedCommand=self.refresh_list)
self.scroll_list = cmds.textScrollList(
numberOfRows=15,
allowMultiSelection=False,
selectCommand=self.on_search_select,
doubleClickCommand=self.execute_source_call
)
cmds.text(label="History (Recent 5):", align="left", height=20)
self.history_list = cmds.textScrollList(
numberOfRows=5,
allowMultiSelection=False,
selectCommand=self.on_history_select,
doubleClickCommand=self.execute_source_call
)
cmds.text(label="Custom Command (Optional):", align="left", height=20)
self.custom_cmd_field = cmds.textField(placeholderText="Function name")
cmds.rowLayout(numberOfColumns=3, adjustableColumn=3, columnWidth3=[60, 60, 70])
cmds.button(label="Source", command=self.execute_source, height=40)
cmds.button(label="Call", command=self.execute_call, height=40)
cmds.button(label="Source / Call", command=self.execute_source_call, height=40, backgroundColor=(0.3, 0.5, 0.3))
cmds.setParent('..')
cmds.setParent('..')
right_form = cmds.formLayout(numberOfDivisions=100)
preview_label = cmds.text(label="Code Preview:", align="left", height=20)
self.preview_field = cmds.scrollField(editable=False, wordWrap=False, font="fixedWidthFont")
cmds.formLayout(right_form, edit=True,
attachForm=[
(preview_label, 'top', 0), (preview_label, 'left', 5), (preview_label, 'right', 5),
(self.preview_field, 'left', 5), (self.preview_field, 'right', 8), (self.preview_field, 'bottom', 8)
],
attachControl=[(self.preview_field, 'top', 5, preview_label)]
)
cmds.setParent('..')
self.refresh_list()
self.refresh_history_ui()
cmds.showWindow(self.window_id)
def update_history_var(self, script_name):
if not script_name:
return
current_history = []
if cmds.optionVar(exists=self.history_var):
current_history = cmds.optionVar(q=self.history_var)
current_history = list(current_history) if isinstance(current_history, (list, tuple)) else ([current_history] if current_history else [])
if script_name in current_history:
current_history.remove(script_name)
current_history.insert(0, script_name)
current_history = current_history[:5]
cmds.optionVar(clearArray=self.history_var)
for item in current_history:
cmds.optionVar(stringValueAppend=(self.history_var, item))
self.refresh_history_ui()
def refresh_history_ui(self):
cmds.textScrollList(self.history_list, edit=True, removeAll=True)
if cmds.optionVar(exists=self.history_var):
history = cmds.optionVar(q=self.history_var)
if history:
cmds.textScrollList(self.history_list, edit=True, append=history)
def read_file_safe(self, path):
encodings = ['utf-8', 'cp932', 'euc-jp', 'utf-16']
for enc in encodings:
try:
with open(path, "r", encoding=enc) as f:
return f.read()
except (UnicodeDecodeError, LookupError):
continue
with open(path, "r", encoding="utf-8", errors="replace") as f:
return f.read()
def refresh_list(self, *args):
search_query = cmds.textField(self.search_field, q=True, text=True).lower()
keywords = search_query.split()
cmds.textScrollList(self.scroll_list, edit=True, removeAll=True)
sorted_names = sorted(self.script_data.keys())
for name in sorted_names:
if all(kw in name.lower() for kw in keywords):
cmds.textScrollList(self.scroll_list, edit=True, append=name)
def update_preview(self, *args):
selected = cmds.textScrollList(self.scroll_list, q=True, selectItem=True)
if not selected:
selected = cmds.textScrollList(self.history_list, q=True, selectItem=True)
if not selected:
cmds.scrollField(self.preview_field, edit=True, text="")
return
path = self.script_data[selected[0]]
content = self.read_file_safe(path)
cmds.scrollField(self.preview_field, edit=True, text=content)
def on_search_select(self):
cmds.textScrollList(self.history_list, edit=True, deselectAll=True)
self.update_preview()
def on_history_select(self):
cmds.textScrollList(self.scroll_list, edit=True, deselectAll=True)
self.update_preview()
def get_active_selection(self):
sel = cmds.textScrollList(self.scroll_list, q=True, selectItem=True)
if not sel:
sel = cmds.textScrollList(self.history_list, q=True, selectItem=True)
return sel[0] if sel else None
def has_mel_execution(self, file_path):
if not os.path.exists(file_path):
return 0
content = self.read_file_safe(file_path)
content = re.sub(r'//.*', lambda m: " " * len(m.group()), content)
content = re.sub(r'/\*.*?\*/', lambda m: " " * len(m.group()), content, flags=re.DOTALL)
defined_procs = set(re.findall(r'\bproc\s+([a-zA-Z_]\w*)', content))
chars = list(content)
n = len(chars)
i = 0
proc_start_pattern = re.compile(r'\bproc\s+\w+')
print(f"\n{'='*80}\n[DEBUG: SCOPE ANALYSIS] {os.path.basename(file_path)}\n{'='*80}")
while i < n:
match = proc_start_pattern.match(content, i)
if match:
actual_start = i
prefix_area = content[max(0, i-60):i]
boundary = max(prefix_area.rfind(';'), prefix_area.rfind('\n')) + 1
prefix_candidate = prefix_area[boundary:].strip()
if prefix_candidate:
actual_start = (max(0, i-60) + boundary) + prefix_area[boundary:].find(prefix_candidate)
start_line = content.count('\n', 0, actual_start) + 1
brace_level = 0
in_proc_body = False
search_idx = i
proc_name = "Unknown"
while search_idx < n:
if chars[search_idx] == '{':
brace_level = 1
in_proc_body = True
for k in range(actual_start, search_idx + 1):
chars[k] = ' '
i = search_idx + 1
break
search_idx += 1
if in_proc_body:
while i < n and brace_level > 0:
if chars[i] == '{':
brace_level += 1
elif chars[i] == '}':
brace_level -= 1
if brace_level == 0:
end_line = content.count('\n', 0, i) + 1
print(f"Found Proc: {proc_name:<30 ---="" .join="" 1="" 80="" be="" break="" case="" catch="" chars="" clean_content.splitlines="" clean_content="" code="" continue="" debug_lines="" default="" do="" else="" end_line="" float="" for="" global="" i="" if="" in="" int="" investigated="" join="" line.strip="" line="" lines="" matrix="" n="" o="" outside="" print="" procedures="" remains="" reserved_words="{" return="" source="" start_line="" string="" switch="" to="" tokens="re.sub(r" vector="" void="" while="">,\n\t\r\"\'`]', ' ', clean_content).split()
found_any = 0
seen = set()
for word in tokens:
if (word in reserved_words or word.startswith('$') or word[0].isdigit() or len(word) <= 1 or word in seen):
continue
seen.add(word)
if word in defined_procs:
print(f"[MATCH] Local Call: '{word}'")
found_any = 1
continue
try:
res = mel.eval(f'whatIs "{word}"').lower()
if any(t in res for t in ['command', 'procedure', 'script']):
print(f"[MATCH] {res.capitalize()}: '{word}'")
found_any = 1
except: continue
return 1 if found_any else 0
def has_python_execution(self, file_path):
# determine whether a Python script contains an executable statement
print(f"\n{'='*60}")
print(f"[TARGET]: {file_path}")
if not os.path.exists(file_path):
print("[ERROR] File not found.")
return 0
try:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
source = f.read()
tree = ast.parse(source)
except Exception as e:
print(f"[ERROR] Syntax or Read Error: {e}")
return 0
found_execution = 0
print(f"[LOG] Analyzing top-level nodes...")
for node in tree.body:
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Import, ast.ImportFrom)):
continue
if isinstance(node, ast.Expr) and isinstance(node.value, (ast.Str, ast.Constant)):
continue
if isinstance(node, ast.If):
test = node.test
if isinstance(test, ast.Compare) and isinstance(test.left, ast.Name) and test.left.id == '__name__':
if isinstance(test.ops[0], ast.Eq):
comp = test.comparators[0]
val = comp.value if isinstance(comp, ast.Constant) else getattr(comp, 's', None)
if val == '__main__':
print("[SKIP] True main block detected.")
continue
found_execution = 1
print(f"[MATCH!] Actual execution node: {ast.dump(node)}")
break
print(f"[RESULT]: {'1 (Execution Found)' if found_execution else '0 (Definitions Only)'}")
print(f"{'='*60}\n")
return found_execution
def execute_source(self, *args):
selected = self.get_active_selection()
if not selected: return
self.update_history_var(selected)
file_name = selected
full_path = self.script_data[file_name]
print(f"\n# --- Sourcing: {file_name} --- #")
try:
if file_name.endswith(".mel"):
mel.eval(f'source "{full_path}";')
elif file_name.endswith(".py"):
code = self.read_file_safe(full_path)
exec(code, globals())
except Exception as e:
cmds.warning("Source failed. See Script Editor for details.")
print(f"# --- ERROR: {e} --- #")
def execute_call(self, *args):
selected = self.get_active_selection()
if not selected: return
self.update_history_var(selected)
file_name = selected
module_name = os.path.splitext(file_name)[0]
input_cmd = cmds.textField(self.custom_cmd_field, q=True, text=True).strip()
exec_name = input_cmd.replace("()", "") if input_cmd else module_name
try:
if file_name.endswith(".mel"):
exists_val = mel.eval(f'exists "{exec_name}"')
what_is_val = mel.eval(f'whatIs "{exec_name}"')
if exists_val and ("procedure" in what_is_val.lower() or "script" in what_is_val.lower()):
print(f"\n# --- Calling: {exec_name} --- #")
mel.eval(f'{exec_name}();')
elif exists_val and what_is_val == "command":
print(f"\n# --- Calling: {exec_name} --- #")
mel.eval(f'{exec_name};')
else:
print(f"Target '{exec_name}' not found in current session.")
elif file_name.endswith(".py"):
found = False
if exec_name in globals():
func = globals()[exec_name]
if callable(func):
func()
found = True
print(f"# Success: Found '{exec_name}' in globals.")
if not found:
try:
if module_name in sys.modules:
module = sys.modules[module_name]
else:
module = importlib.import_module(module_name)
if hasattr(module, exec_name):
func = getattr(module, exec_name)
if callable(func):
func()
print(f"# Success: {module_name}.{exec_name}() called.")
found = True
else:
if callable(module):
module()
found = True
except Exception as e:
print(f"# Error: Failed to import or execute '{module_name}': {e}")
if not found:
print(f"# Warning: Target '{exec_name}' not found in current session or module '{module_name}'.")
except Exception as e:
cmds.warning("Call failed. See Script Editor for details.")
print(f"# --- ERROR: {e} --- #")
def execute_source_call(self, *args):
selected = self.get_active_selection()
if not selected: return
self.update_history_var(selected)
file_name = selected
full_path = self.script_data[file_name]
module_name = os.path.splitext(file_name)[0]
input_cmd = cmds.textField(self.custom_cmd_field, q=True, text=True).strip()
exec_name = input_cmd.replace("()", "") if input_cmd else module_name
try:
if file_name.endswith(".mel"):
exists_val = mel.eval(f'exists "{exec_name}"')
what_is_val = mel.eval(f'whatIs "{exec_name}"')
hasExecution = self.has_mel_execution(full_path)
if hasExecution:
print(f"# --- Sourcing: {file_name} --- #")
mel.eval(f'source "{full_path}";')
elif exists_val and ("procedure" in what_is_val.lower() or "script" in what_is_val.lower()):
print(f"\n# --- Calling: {exec_name} --- #")
mel.eval(f'{exec_name}();')
elif exists_val and what_is_val == "Command":
print(f"\n# --- Calling: {exec_name} --- #")
mel.eval(f'{exec_name};')
elif file_name.endswith(".py"):
found = False
hasExecution = self.has_python_execution(full_path)
if hasExecution:
print(f"# --- Sourcing: {file_name} --- #")
code = self.read_file_safe(full_path)
exec(code, globals())
else:
if exec_name in globals():
func = globals()[exec_name]
if callable(func):
print(f"\n# --- Calling: {exec_name} --- #")
func()
found = True
if not found:
try:
if module_name in sys.modules:
module = sys.modules[module_name]
else:
module = importlib.import_module(module_name)
if hasattr(module, exec_name):
func = getattr(module, exec_name)
if callable(func):
print(f"\n# --- Calling: {module_name}.{exec_name}() --- #")
func()
found = True
else:
if callable(module):
module()
found = True
except Exception as e:
print(f"# Error: Failed to import or execute '{module_name}': {e}")
if not found:
print(f"# Warning: Target '{exec_name}' not found in current session or module '{module_name}'.")
except Exception as e:
cmds.warning("Call failed. See Script Editor for details.")
print(f"# --- ERROR: {e} --- #")
launcher = StmScriptLauncher()30>補足
シェルフに追加する
以下の手順でシェルフに追加できます。
- スクリプトエディタに全文ペースト(※Pythonタブを使用)
- 貼り付けたスクリプトを全選択(Ctrl+A)
- シェルフに中ボタンドラッグ
シェルフのTooltip 表示について
シェルフアイコンにカーソルをホバーさせたときにこのスクリプト全文が表示されないようにするには、Tooltipを編集します。
アイコンを右クリックしてShelf Editorを開いて編集します。
Shelf Editor:
マーキングメニューへ追加する
もしマーキングメニューに入れたくなったら、
- StmScriptLauncher.py の名前でスクリプトファイルを保存
- MEL化した以下のスクリプトをシェルフに登録
python("import StmScriptLauncher;StmScriptLauncher.StmScriptLauncher(); None"); - マーキングメニューに追加
という手順を踏むのが簡潔かと思います。
おわりに
あらゆるスクリプトを使用するのに利用できる、汎用性の高いスクリプトです。
非常に有用性の高いものかと思います。
皆さんの参考になれば幸いです。

