Skip to content

Commit

Permalink
Merge pull request #1045 from nstelter-slac/shell_cmd_bash_option
Browse files Browse the repository at this point in the history
PyDMShellCommand bash option
  • Loading branch information
jbellister-slac authored Nov 7, 2023
2 parents e1dcbe0 + 1df62d4 commit 94f92de
Show file tree
Hide file tree
Showing 3 changed files with 294 additions and 1 deletion.
5 changes: 5 additions & 0 deletions examples/shell_command/example_cmd.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/sh

# This script can be called by a PyDMShellCommand widget,
# allowing it to make use of command chaining and other shell features.
echo "Hello World!" && echo "Hello Again!"
256 changes: 256 additions & 0 deletions examples/shell_command/shell_command_full_shell.ui
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>458</width>
<height>386</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The PyDMShellCommand button with run your command through a full shell if you enable the 'runCommandsInFullShell' option. This allows you to use some additional features such as shell syntax ('|', '&amp;amp;', ';', etc), environment variables ($VAR), glob expansion ('*', '?', etc), and some other features.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="PyDMShellCommand" name="PyDMShellCommand">
<property name="toolTip">
<string/>
</property>
<property name="whatsThis">
<string/>
</property>
<property name="text">
<string>runCmdsInFullShell Option Enabled</string>
</property>
<property name="alarmSensitiveContent" stdset="0">
<bool>false</bool>
</property>
<property name="alarmSensitiveBorder" stdset="0">
<bool>true</bool>
</property>
<property name="PyDMToolTip" stdset="0">
<string/>
</property>
<property name="channel" stdset="0">
<string/>
</property>
<property name="showConfirmDialog" stdset="0">
<bool>false</bool>
</property>
<property name="runCommandsInFullShell" stdset="0">
<bool>true</bool>
</property>
<property name="confirmMessage" stdset="0">
<string>Are you sure you want to proceed?</string>
</property>
<property name="environmentVariables" stdset="0">
<string/>
</property>
<property name="showIcon" stdset="0">
<bool>true</bool>
</property>
<property name="redirectCommandOutput" stdset="0">
<bool>true</bool>
</property>
<property name="allowMultipleExecutions" stdset="0">
<bool>false</bool>
</property>
<property name="titles" stdset="0">
<stringlist/>
</property>
<property name="commands" stdset="0">
<stringlist>
<string>echo First; echo Second</string>
</stringlist>
</property>
<property name="passwordProtected" stdset="0">
<bool>false</bool>
</property>
<property name="password" stdset="0">
<string/>
</property>
<property name="protectedPassword" stdset="0">
<string/>
</property>
<property name="runCommandsInBash" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>You can run through a Bash shell (without needing to enable any options) by specifying &quot;bash -c&quot; at the start of your command. </string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="PyDMShellCommand" name="PyDMShellCommand_3">
<property name="toolTip">
<string/>
</property>
<property name="whatsThis">
<string/>
</property>
<property name="text">
<string>Using &quot;-c bash&quot;</string>
</property>
<property name="alarmSensitiveContent" stdset="0">
<bool>false</bool>
</property>
<property name="alarmSensitiveBorder" stdset="0">
<bool>true</bool>
</property>
<property name="PyDMToolTip" stdset="0">
<string/>
</property>
<property name="channel" stdset="0">
<string/>
</property>
<property name="showConfirmDialog" stdset="0">
<bool>false</bool>
</property>
<property name="confirmMessage" stdset="0">
<string>Are you sure you want to proceed?</string>
</property>
<property name="environmentVariables" stdset="0">
<string/>
</property>
<property name="showIcon" stdset="0">
<bool>true</bool>
</property>
<property name="redirectCommandOutput" stdset="0">
<bool>true</bool>
</property>
<property name="allowMultipleExecutions" stdset="0">
<bool>false</bool>
</property>
<property name="titles" stdset="0">
<stringlist/>
</property>
<property name="commands" stdset="0">
<stringlist>
<string>bash -c &quot;echo 'Hello One'; echo 'Hello two'&quot;</string>
</stringlist>
</property>
<property name="passwordProtected" stdset="0">
<bool>false</bool>
</property>
<property name="password" stdset="0">
<string/>
</property>
<property name="protectedPassword" stdset="0">
<string/>
</property>
<property name="runCommandsInBash" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>You can also call a shell script. For this button to work correctly, run pydm from dir 'examples/shell_command' so it can find the script file.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="PyDMShellCommand" name="PyDMShellCommand_2">
<property name="toolTip">
<string/>
</property>
<property name="text">
<string>Calling external script</string>
</property>
<property name="alarmSensitiveContent" stdset="0">
<bool>false</bool>
</property>
<property name="alarmSensitiveBorder" stdset="0">
<bool>true</bool>
</property>
<property name="PyDMToolTip" stdset="0">
<string/>
</property>
<property name="channel" stdset="0">
<string/>
</property>
<property name="showConfirmDialog" stdset="0">
<bool>false</bool>
</property>
<property name="confirmMessage" stdset="0">
<string>Are you sure you want to proceed?</string>
</property>
<property name="environmentVariables" stdset="0">
<string/>
</property>
<property name="showIcon" stdset="0">
<bool>true</bool>
</property>
<property name="redirectCommandOutput" stdset="0">
<bool>true</bool>
</property>
<property name="allowMultipleExecutions" stdset="0">
<bool>false</bool>
</property>
<property name="titles" stdset="0">
<stringlist>
<string>Print &quot;Hello, World!&quot; to terminal</string>
<string>Print current working directory to terminal</string>
</stringlist>
</property>
<property name="commands" stdset="0">
<stringlist>
<string>./example_cmd.sh</string>
</stringlist>
</property>
<property name="passwordProtected" stdset="0">
<bool>false</bool>
</property>
<property name="password" stdset="0">
<string/>
</property>
<property name="protectedPassword" stdset="0">
<string/>
</property>
<property name="runCommandsInBash" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>PyDMShellCommand</class>
<extends>QPushButton</extends>
<header>pydm.widgets.shell_command</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>
34 changes: 33 additions & 1 deletion pydm/widgets/shell_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ def __init__(
self.process = None
self._show_icon = True
self._redirect_output = False
# shell allows for more options such as command chaining ("cmd1;cmd2", "cmd1 && cmd2", etc ...),
# use of environment variables, glob expansion ('ls *.txt'), etc...
self._run_commands_in_full_shell = False

self._password_protected = False
self._password = ""
Expand Down Expand Up @@ -203,6 +206,29 @@ def showConfirmDialog(self, value: bool) -> None:
if self._show_confirm_dialog != value:
self._show_confirm_dialog = value

@Property(bool)
def runCommandsInFullShell(self) -> bool:
"""
Whether or not to run cmds with Popen's option for running them through a shell subprocess.
Returns
-------
bool
"""
return self._run_commands_in_full_shell

@runCommandsInFullShell.setter
def runCommandsInFullShell(self, value: bool) -> None:
"""
Whether or not to run cmds with Popen's option for running them through a shell subprocess.
Parameters
----------
value : bool
"""
if self._run_commands_in_full_shell != value:
self._run_commands_in_full_shell = value

@Property(str)
def confirmMessage(self) -> str:
"""
Expand Down Expand Up @@ -603,6 +629,9 @@ def execute_command(self, command: str) -> None:
if (self.process is None or self.process.poll() is not None) or self._allow_multiple:
cmd = os.path.expanduser(os.path.expandvars(command))
args = shlex.split(cmd, posix="win" not in sys.platform)
# when shell enabled, Popen should take the cmds as a single string (not list)
if self._run_commands_in_full_shell:
args = cmd
try:
logger.debug("Launching process: %s", repr(args))
stdout = subprocess.PIPE
Expand All @@ -614,7 +643,10 @@ def execute_command(self, command: str) -> None:

if self._redirect_output:
stdout = None
self.process = subprocess.Popen(args, stdout=stdout, stderr=subprocess.PIPE, env=env_var)
self.process = subprocess.Popen(
args, stdout=stdout, stderr=subprocess.PIPE, env=env_var, shell=self._run_commands_in_full_shell
)

except Exception as exc:
self.show_warning_icon()
logger.error("Error in shell command: %s", exc)
Expand Down

0 comments on commit 94f92de

Please sign in to comment.