Skip to content

Commit 94f92de

Browse files
Merge pull request #1045 from nstelter-slac/shell_cmd_bash_option
PyDMShellCommand bash option
2 parents e1dcbe0 + 1df62d4 commit 94f92de

File tree

3 files changed

+294
-1
lines changed

3 files changed

+294
-1
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/bin/sh
2+
3+
# This script can be called by a PyDMShellCommand widget,
4+
# allowing it to make use of command chaining and other shell features.
5+
echo "Hello World!" && echo "Hello Again!"
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<ui version="4.0">
3+
<class>Form</class>
4+
<widget class="QWidget" name="Form">
5+
<property name="geometry">
6+
<rect>
7+
<x>0</x>
8+
<y>0</y>
9+
<width>458</width>
10+
<height>386</height>
11+
</rect>
12+
</property>
13+
<property name="windowTitle">
14+
<string>Form</string>
15+
</property>
16+
<layout class="QVBoxLayout" name="verticalLayout">
17+
<item>
18+
<widget class="QLabel" name="label">
19+
<property name="sizePolicy">
20+
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
21+
<horstretch>0</horstretch>
22+
<verstretch>0</verstretch>
23+
</sizepolicy>
24+
</property>
25+
<property name="text">
26+
<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>
27+
</property>
28+
<property name="wordWrap">
29+
<bool>true</bool>
30+
</property>
31+
</widget>
32+
</item>
33+
<item>
34+
<widget class="PyDMShellCommand" name="PyDMShellCommand">
35+
<property name="toolTip">
36+
<string/>
37+
</property>
38+
<property name="whatsThis">
39+
<string/>
40+
</property>
41+
<property name="text">
42+
<string>runCmdsInFullShell Option Enabled</string>
43+
</property>
44+
<property name="alarmSensitiveContent" stdset="0">
45+
<bool>false</bool>
46+
</property>
47+
<property name="alarmSensitiveBorder" stdset="0">
48+
<bool>true</bool>
49+
</property>
50+
<property name="PyDMToolTip" stdset="0">
51+
<string/>
52+
</property>
53+
<property name="channel" stdset="0">
54+
<string/>
55+
</property>
56+
<property name="showConfirmDialog" stdset="0">
57+
<bool>false</bool>
58+
</property>
59+
<property name="runCommandsInFullShell" stdset="0">
60+
<bool>true</bool>
61+
</property>
62+
<property name="confirmMessage" stdset="0">
63+
<string>Are you sure you want to proceed?</string>
64+
</property>
65+
<property name="environmentVariables" stdset="0">
66+
<string/>
67+
</property>
68+
<property name="showIcon" stdset="0">
69+
<bool>true</bool>
70+
</property>
71+
<property name="redirectCommandOutput" stdset="0">
72+
<bool>true</bool>
73+
</property>
74+
<property name="allowMultipleExecutions" stdset="0">
75+
<bool>false</bool>
76+
</property>
77+
<property name="titles" stdset="0">
78+
<stringlist/>
79+
</property>
80+
<property name="commands" stdset="0">
81+
<stringlist>
82+
<string>echo First; echo Second</string>
83+
</stringlist>
84+
</property>
85+
<property name="passwordProtected" stdset="0">
86+
<bool>false</bool>
87+
</property>
88+
<property name="password" stdset="0">
89+
<string/>
90+
</property>
91+
<property name="protectedPassword" stdset="0">
92+
<string/>
93+
</property>
94+
<property name="runCommandsInBash" stdset="0">
95+
<bool>true</bool>
96+
</property>
97+
</widget>
98+
</item>
99+
<item>
100+
<widget class="QLabel" name="label_2">
101+
<property name="text">
102+
<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>
103+
</property>
104+
<property name="wordWrap">
105+
<bool>true</bool>
106+
</property>
107+
</widget>
108+
</item>
109+
<item>
110+
<widget class="PyDMShellCommand" name="PyDMShellCommand_3">
111+
<property name="toolTip">
112+
<string/>
113+
</property>
114+
<property name="whatsThis">
115+
<string/>
116+
</property>
117+
<property name="text">
118+
<string>Using &quot;-c bash&quot;</string>
119+
</property>
120+
<property name="alarmSensitiveContent" stdset="0">
121+
<bool>false</bool>
122+
</property>
123+
<property name="alarmSensitiveBorder" stdset="0">
124+
<bool>true</bool>
125+
</property>
126+
<property name="PyDMToolTip" stdset="0">
127+
<string/>
128+
</property>
129+
<property name="channel" stdset="0">
130+
<string/>
131+
</property>
132+
<property name="showConfirmDialog" stdset="0">
133+
<bool>false</bool>
134+
</property>
135+
<property name="confirmMessage" stdset="0">
136+
<string>Are you sure you want to proceed?</string>
137+
</property>
138+
<property name="environmentVariables" stdset="0">
139+
<string/>
140+
</property>
141+
<property name="showIcon" stdset="0">
142+
<bool>true</bool>
143+
</property>
144+
<property name="redirectCommandOutput" stdset="0">
145+
<bool>true</bool>
146+
</property>
147+
<property name="allowMultipleExecutions" stdset="0">
148+
<bool>false</bool>
149+
</property>
150+
<property name="titles" stdset="0">
151+
<stringlist/>
152+
</property>
153+
<property name="commands" stdset="0">
154+
<stringlist>
155+
<string>bash -c &quot;echo 'Hello One'; echo 'Hello two'&quot;</string>
156+
</stringlist>
157+
</property>
158+
<property name="passwordProtected" stdset="0">
159+
<bool>false</bool>
160+
</property>
161+
<property name="password" stdset="0">
162+
<string/>
163+
</property>
164+
<property name="protectedPassword" stdset="0">
165+
<string/>
166+
</property>
167+
<property name="runCommandsInBash" stdset="0">
168+
<bool>false</bool>
169+
</property>
170+
</widget>
171+
</item>
172+
<item>
173+
<widget class="QLabel" name="label_3">
174+
<property name="text">
175+
<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>
176+
</property>
177+
<property name="wordWrap">
178+
<bool>true</bool>
179+
</property>
180+
</widget>
181+
</item>
182+
<item>
183+
<widget class="PyDMShellCommand" name="PyDMShellCommand_2">
184+
<property name="toolTip">
185+
<string/>
186+
</property>
187+
<property name="text">
188+
<string>Calling external script</string>
189+
</property>
190+
<property name="alarmSensitiveContent" stdset="0">
191+
<bool>false</bool>
192+
</property>
193+
<property name="alarmSensitiveBorder" stdset="0">
194+
<bool>true</bool>
195+
</property>
196+
<property name="PyDMToolTip" stdset="0">
197+
<string/>
198+
</property>
199+
<property name="channel" stdset="0">
200+
<string/>
201+
</property>
202+
<property name="showConfirmDialog" stdset="0">
203+
<bool>false</bool>
204+
</property>
205+
<property name="confirmMessage" stdset="0">
206+
<string>Are you sure you want to proceed?</string>
207+
</property>
208+
<property name="environmentVariables" stdset="0">
209+
<string/>
210+
</property>
211+
<property name="showIcon" stdset="0">
212+
<bool>true</bool>
213+
</property>
214+
<property name="redirectCommandOutput" stdset="0">
215+
<bool>true</bool>
216+
</property>
217+
<property name="allowMultipleExecutions" stdset="0">
218+
<bool>false</bool>
219+
</property>
220+
<property name="titles" stdset="0">
221+
<stringlist>
222+
<string>Print &quot;Hello, World!&quot; to terminal</string>
223+
<string>Print current working directory to terminal</string>
224+
</stringlist>
225+
</property>
226+
<property name="commands" stdset="0">
227+
<stringlist>
228+
<string>./example_cmd.sh</string>
229+
</stringlist>
230+
</property>
231+
<property name="passwordProtected" stdset="0">
232+
<bool>false</bool>
233+
</property>
234+
<property name="password" stdset="0">
235+
<string/>
236+
</property>
237+
<property name="protectedPassword" stdset="0">
238+
<string/>
239+
</property>
240+
<property name="runCommandsInBash" stdset="0">
241+
<bool>false</bool>
242+
</property>
243+
</widget>
244+
</item>
245+
</layout>
246+
</widget>
247+
<customwidgets>
248+
<customwidget>
249+
<class>PyDMShellCommand</class>
250+
<extends>QPushButton</extends>
251+
<header>pydm.widgets.shell_command</header>
252+
</customwidget>
253+
</customwidgets>
254+
<resources/>
255+
<connections/>
256+
</ui>

pydm/widgets/shell_command.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ def __init__(
6868
self.process = None
6969
self._show_icon = True
7070
self._redirect_output = False
71+
# shell allows for more options such as command chaining ("cmd1;cmd2", "cmd1 && cmd2", etc ...),
72+
# use of environment variables, glob expansion ('ls *.txt'), etc...
73+
self._run_commands_in_full_shell = False
7174

7275
self._password_protected = False
7376
self._password = ""
@@ -203,6 +206,29 @@ def showConfirmDialog(self, value: bool) -> None:
203206
if self._show_confirm_dialog != value:
204207
self._show_confirm_dialog = value
205208

209+
@Property(bool)
210+
def runCommandsInFullShell(self) -> bool:
211+
"""
212+
Whether or not to run cmds with Popen's option for running them through a shell subprocess.
213+
214+
Returns
215+
-------
216+
bool
217+
"""
218+
return self._run_commands_in_full_shell
219+
220+
@runCommandsInFullShell.setter
221+
def runCommandsInFullShell(self, value: bool) -> None:
222+
"""
223+
Whether or not to run cmds with Popen's option for running them through a shell subprocess.
224+
225+
Parameters
226+
----------
227+
value : bool
228+
"""
229+
if self._run_commands_in_full_shell != value:
230+
self._run_commands_in_full_shell = value
231+
206232
@Property(str)
207233
def confirmMessage(self) -> str:
208234
"""
@@ -603,6 +629,9 @@ def execute_command(self, command: str) -> None:
603629
if (self.process is None or self.process.poll() is not None) or self._allow_multiple:
604630
cmd = os.path.expanduser(os.path.expandvars(command))
605631
args = shlex.split(cmd, posix="win" not in sys.platform)
632+
# when shell enabled, Popen should take the cmds as a single string (not list)
633+
if self._run_commands_in_full_shell:
634+
args = cmd
606635
try:
607636
logger.debug("Launching process: %s", repr(args))
608637
stdout = subprocess.PIPE
@@ -614,7 +643,10 @@ def execute_command(self, command: str) -> None:
614643

615644
if self._redirect_output:
616645
stdout = None
617-
self.process = subprocess.Popen(args, stdout=stdout, stderr=subprocess.PIPE, env=env_var)
646+
self.process = subprocess.Popen(
647+
args, stdout=stdout, stderr=subprocess.PIPE, env=env_var, shell=self._run_commands_in_full_shell
648+
)
649+
618650
except Exception as exc:
619651
self.show_warning_icon()
620652
logger.error("Error in shell command: %s", exc)

0 commit comments

Comments
 (0)