##// END OF EJS Templates
Use proper subprocess commands instead of modifying the command line....
Matthias Bussonnier -
Show More
@@ -1,366 +1,372 b''
1 1 # encoding: utf-8
2 2 """
3 3 Paging capabilities for IPython.core
4 4
5 5 Notes
6 6 -----
7 7
8 8 For now this uses IPython hooks, so it can't be in IPython.utils. If we can get
9 9 rid of that dependency, we could move it there.
10 10 -----
11 11 """
12 12
13 13 # Copyright (c) IPython Development Team.
14 14 # Distributed under the terms of the Modified BSD License.
15 15
16 16
17 17 import os
18 import io
18 19 import re
19 20 import sys
20 21 import tempfile
22 import subprocess
21 23
22 24 from io import UnsupportedOperation
23 25
24 26 from IPython import get_ipython
25 27 from IPython.core.display import display
26 28 from IPython.core.error import TryNext
27 29 from IPython.utils.data import chop
28 30 from IPython.utils.process import system
29 31 from IPython.utils.terminal import get_terminal_size
30 32 from IPython.utils import py3compat
31 33
32 34
33 35 def display_page(strng, start=0, screen_lines=25):
34 36 """Just display, no paging. screen_lines is ignored."""
35 37 if isinstance(strng, dict):
36 38 data = strng
37 39 else:
38 40 if start:
39 41 strng = u'\n'.join(strng.splitlines()[start:])
40 42 data = { 'text/plain': strng }
41 43 display(data, raw=True)
42 44
43 45
44 46 def as_hook(page_func):
45 47 """Wrap a pager func to strip the `self` arg
46 48
47 49 so it can be called as a hook.
48 50 """
49 51 return lambda self, *args, **kwargs: page_func(*args, **kwargs)
50 52
51 53
52 54 esc_re = re.compile(r"(\x1b[^m]+m)")
53 55
54 56 def page_dumb(strng, start=0, screen_lines=25):
55 57 """Very dumb 'pager' in Python, for when nothing else works.
56 58
57 59 Only moves forward, same interface as page(), except for pager_cmd and
58 60 mode.
59 61 """
60 62 if isinstance(strng, dict):
61 63 strng = strng.get('text/plain', '')
62 64 out_ln = strng.splitlines()[start:]
63 65 screens = chop(out_ln,screen_lines-1)
64 66 if len(screens) == 1:
65 67 print(os.linesep.join(screens[0]))
66 68 else:
67 69 last_escape = ""
68 70 for scr in screens[0:-1]:
69 71 hunk = os.linesep.join(scr)
70 72 print(last_escape + hunk)
71 73 if not page_more():
72 74 return
73 75 esc_list = esc_re.findall(hunk)
74 76 if len(esc_list) > 0:
75 77 last_escape = esc_list[-1]
76 78 print(last_escape + os.linesep.join(screens[-1]))
77 79
78 80 def _detect_screen_size(screen_lines_def):
79 81 """Attempt to work out the number of lines on the screen.
80 82
81 83 This is called by page(). It can raise an error (e.g. when run in the
82 84 test suite), so it's separated out so it can easily be called in a try block.
83 85 """
84 86 TERM = os.environ.get('TERM',None)
85 87 if not((TERM=='xterm' or TERM=='xterm-color') and sys.platform != 'sunos5'):
86 88 # curses causes problems on many terminals other than xterm, and
87 89 # some termios calls lock up on Sun OS5.
88 90 return screen_lines_def
89 91
90 92 try:
91 93 import termios
92 94 import curses
93 95 except ImportError:
94 96 return screen_lines_def
95 97
96 98 # There is a bug in curses, where *sometimes* it fails to properly
97 99 # initialize, and then after the endwin() call is made, the
98 100 # terminal is left in an unusable state. Rather than trying to
99 101 # check every time for this (by requesting and comparing termios
100 102 # flags each time), we just save the initial terminal state and
101 103 # unconditionally reset it every time. It's cheaper than making
102 104 # the checks.
103 105 try:
104 106 term_flags = termios.tcgetattr(sys.stdout)
105 107 except termios.error as err:
106 108 # can fail on Linux 2.6, pager_page will catch the TypeError
107 109 raise TypeError('termios error: {0}'.format(err))
108 110
109 111 try:
110 112 scr = curses.initscr()
111 113 except AttributeError:
112 114 # Curses on Solaris may not be complete, so we can't use it there
113 115 return screen_lines_def
114 116
115 117 screen_lines_real,screen_cols = scr.getmaxyx()
116 118 curses.endwin()
117 119
118 120 # Restore terminal state in case endwin() didn't.
119 121 termios.tcsetattr(sys.stdout,termios.TCSANOW,term_flags)
120 122 # Now we have what we needed: the screen size in rows/columns
121 123 return screen_lines_real
122 124 #print '***Screen size:',screen_lines_real,'lines x',\
123 125 #screen_cols,'columns.' # dbg
124 126
125 127 def pager_page(strng, start=0, screen_lines=0, pager_cmd=None):
126 128 """Display a string, piping through a pager after a certain length.
127 129
128 130 strng can be a mime-bundle dict, supplying multiple representations,
129 131 keyed by mime-type.
130 132
131 133 The screen_lines parameter specifies the number of *usable* lines of your
132 134 terminal screen (total lines minus lines you need to reserve to show other
133 135 information).
134 136
135 137 If you set screen_lines to a number <=0, page() will try to auto-determine
136 138 your screen size and will only use up to (screen_size+screen_lines) for
137 139 printing, paging after that. That is, if you want auto-detection but need
138 140 to reserve the bottom 3 lines of the screen, use screen_lines = -3, and for
139 141 auto-detection without any lines reserved simply use screen_lines = 0.
140 142
141 143 If a string won't fit in the allowed lines, it is sent through the
142 144 specified pager command. If none given, look for PAGER in the environment,
143 145 and ultimately default to less.
144 146
145 147 If no system pager works, the string is sent through a 'dumb pager'
146 148 written in python, very simplistic.
147 149 """
148 150
149 151 # for compatibility with mime-bundle form:
150 152 if isinstance(strng, dict):
151 153 strng = strng['text/plain']
152 154
153 155 # Ugly kludge, but calling curses.initscr() flat out crashes in emacs
154 156 TERM = os.environ.get('TERM','dumb')
155 157 if TERM in ['dumb','emacs'] and os.name != 'nt':
156 158 print(strng)
157 159 return
158 160 # chop off the topmost part of the string we don't want to see
159 161 str_lines = strng.splitlines()[start:]
160 162 str_toprint = os.linesep.join(str_lines)
161 163 num_newlines = len(str_lines)
162 164 len_str = len(str_toprint)
163 165
164 166 # Dumb heuristics to guesstimate number of on-screen lines the string
165 167 # takes. Very basic, but good enough for docstrings in reasonable
166 168 # terminals. If someone later feels like refining it, it's not hard.
167 169 numlines = max(num_newlines,int(len_str/80)+1)
168 170
169 171 screen_lines_def = get_terminal_size()[1]
170 172
171 173 # auto-determine screen size
172 174 if screen_lines <= 0:
173 175 try:
174 176 screen_lines += _detect_screen_size(screen_lines_def)
175 177 except (TypeError, UnsupportedOperation):
176 178 print(str_toprint)
177 179 return
178 180
179 181 #print 'numlines',numlines,'screenlines',screen_lines # dbg
180 182 if numlines <= screen_lines :
181 183 #print '*** normal print' # dbg
182 184 print(str_toprint)
183 185 else:
184 186 # Try to open pager and default to internal one if that fails.
185 187 # All failure modes are tagged as 'retval=1', to match the return
186 188 # value of a failed system command. If any intermediate attempt
187 189 # sets retval to 1, at the end we resort to our own page_dumb() pager.
188 190 pager_cmd = get_pager_cmd(pager_cmd)
189 191 pager_cmd += ' ' + get_pager_start(pager_cmd,start)
190 192 if os.name == 'nt':
191 193 if pager_cmd.startswith('type'):
192 194 # The default WinXP 'type' command is failing on complex strings.
193 195 retval = 1
194 196 else:
195 197 fd, tmpname = tempfile.mkstemp('.txt')
196 198 try:
197 199 os.close(fd)
198 200 with open(tmpname, 'wt') as tmpfile:
199 201 tmpfile.write(strng)
200 202 cmd = "%s < %s" % (pager_cmd, tmpname)
201 203 # tmpfile needs to be closed for windows
202 204 if os.system(cmd):
203 205 retval = 1
204 206 else:
205 207 retval = None
206 208 finally:
207 209 os.remove(tmpname)
208 210 else:
209 211 try:
210 212 retval = None
211 # if I use popen4, things hang. No idea why.
212 #pager,shell_out = os.popen4(pager_cmd)
213 pager = os.popen(pager_cmd + ' 2>/dev/null', 'w')
213 # Emulate os.popen, but redirect stderr
214 proc = subprocess.Popen(pager_cmd,
215 shell=True,
216 stdin=subprocess.PIPE,
217 stderr=subprocess.DEVNULL
218 )
219 pager = os._wrap_close(io.TextIOWrapper(proc.stdin), proc)
214 220 try:
215 221 pager_encoding = pager.encoding or sys.stdout.encoding
216 222 pager.write(strng)
217 223 finally:
218 224 retval = pager.close()
219 225 except IOError as msg: # broken pipe when user quits
220 226 if msg.args == (32, 'Broken pipe'):
221 227 retval = None
222 228 else:
223 229 retval = 1
224 230 except OSError:
225 231 # Other strange problems, sometimes seen in Win2k/cygwin
226 232 retval = 1
227 233 if retval is not None:
228 234 page_dumb(strng,screen_lines=screen_lines)
229 235
230 236
231 237 def page(data, start=0, screen_lines=0, pager_cmd=None):
232 238 """Display content in a pager, piping through a pager after a certain length.
233 239
234 240 data can be a mime-bundle dict, supplying multiple representations,
235 241 keyed by mime-type, or text.
236 242
237 243 Pager is dispatched via the `show_in_pager` IPython hook.
238 244 If no hook is registered, `pager_page` will be used.
239 245 """
240 246 # Some routines may auto-compute start offsets incorrectly and pass a
241 247 # negative value. Offset to 0 for robustness.
242 248 start = max(0, start)
243 249
244 250 # first, try the hook
245 251 ip = get_ipython()
246 252 if ip:
247 253 try:
248 254 ip.hooks.show_in_pager(data, start=start, screen_lines=screen_lines)
249 255 return
250 256 except TryNext:
251 257 pass
252 258
253 259 # fallback on default pager
254 260 return pager_page(data, start, screen_lines, pager_cmd)
255 261
256 262
257 263 def page_file(fname, start=0, pager_cmd=None):
258 264 """Page a file, using an optional pager command and starting line.
259 265 """
260 266
261 267 pager_cmd = get_pager_cmd(pager_cmd)
262 268 pager_cmd += ' ' + get_pager_start(pager_cmd,start)
263 269
264 270 try:
265 271 if os.environ['TERM'] in ['emacs','dumb']:
266 272 raise EnvironmentError
267 273 system(pager_cmd + ' ' + fname)
268 274 except:
269 275 try:
270 276 if start > 0:
271 277 start -= 1
272 278 page(open(fname).read(),start)
273 279 except:
274 280 print('Unable to show file',repr(fname))
275 281
276 282
277 283 def get_pager_cmd(pager_cmd=None):
278 284 """Return a pager command.
279 285
280 286 Makes some attempts at finding an OS-correct one.
281 287 """
282 288 if os.name == 'posix':
283 289 default_pager_cmd = 'less -R' # -R for color control sequences
284 290 elif os.name in ['nt','dos']:
285 291 default_pager_cmd = 'type'
286 292
287 293 if pager_cmd is None:
288 294 try:
289 295 pager_cmd = os.environ['PAGER']
290 296 except:
291 297 pager_cmd = default_pager_cmd
292 298
293 299 if pager_cmd == 'less' and '-r' not in os.environ.get('LESS', '').lower():
294 300 pager_cmd += ' -R'
295 301
296 302 return pager_cmd
297 303
298 304
299 305 def get_pager_start(pager, start):
300 306 """Return the string for paging files with an offset.
301 307
302 308 This is the '+N' argument which less and more (under Unix) accept.
303 309 """
304 310
305 311 if pager in ['less','more']:
306 312 if start:
307 313 start_string = '+' + str(start)
308 314 else:
309 315 start_string = ''
310 316 else:
311 317 start_string = ''
312 318 return start_string
313 319
314 320
315 321 # (X)emacs on win32 doesn't like to be bypassed with msvcrt.getch()
316 322 if os.name == 'nt' and os.environ.get('TERM','dumb') != 'emacs':
317 323 import msvcrt
318 324 def page_more():
319 325 """ Smart pausing between pages
320 326
321 327 @return: True if need print more lines, False if quit
322 328 """
323 329 sys.stdout.write('---Return to continue, q to quit--- ')
324 330 ans = msvcrt.getwch()
325 331 if ans in ("q", "Q"):
326 332 result = False
327 333 else:
328 334 result = True
329 335 sys.stdout.write("\b"*37 + " "*37 + "\b"*37)
330 336 return result
331 337 else:
332 338 def page_more():
333 339 ans = py3compat.input('---Return to continue, q to quit--- ')
334 340 if ans.lower().startswith('q'):
335 341 return False
336 342 else:
337 343 return True
338 344
339 345
340 346 def snip_print(str,width = 75,print_full = 0,header = ''):
341 347 """Print a string snipping the midsection to fit in width.
342 348
343 349 print_full: mode control:
344 350
345 351 - 0: only snip long strings
346 352 - 1: send to page() directly.
347 353 - 2: snip long strings and ask for full length viewing with page()
348 354
349 355 Return 1 if snipping was necessary, 0 otherwise."""
350 356
351 357 if print_full == 1:
352 358 page(header+str)
353 359 return 0
354 360
355 361 print(header, end=' ')
356 362 if len(str) < width:
357 363 print(str)
358 364 snip = 0
359 365 else:
360 366 whalf = int((width -5)/2)
361 367 print(str[:whalf] + ' <...> ' + str[-whalf:])
362 368 snip = 1
363 369 if snip and print_full == 2:
364 370 if py3compat.input(header+' Snipped. View (y/n)? [N]').lower() == 'y':
365 371 page(str)
366 372 return snip
General Comments 0
You need to be logged in to leave comments. Login now