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