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