diff --git a/IPython/core/magics/execution.py b/IPython/core/magics/execution.py index 572b223..3669ef2 100644 --- a/IPython/core/magics/execution.py +++ b/IPython/core/magics/execution.py @@ -44,10 +44,11 @@ from IPython.utils import py3compat from IPython.utils.io import capture_output from IPython.utils.ipstruct import Struct from IPython.utils.module_paths import find_mod -from IPython.utils.path import get_py_filename, unquote_filename +from IPython.utils.path import get_py_filename, unquote_filename, shellglob from IPython.utils.timing import clock, clock2 from IPython.utils.warn import warn, error + #----------------------------------------------------------------------------- # Magic implementation classes #----------------------------------------------------------------------------- @@ -324,7 +325,7 @@ python-profiler package from non-free.""") """Run the named file inside IPython as a program. Usage:\\ - %run [-n -i -t [-N] -d [-b] -p [profile options]] file [args] + %run [-n -i -t [-N] -d [-b] -p [profile options] -G] file [args] Parameters after the filename are passed as command-line arguments to the program (put in sys.argv). Then, control returns to IPython's @@ -345,6 +346,13 @@ python-profiler package from non-free.""") and sys.argv). This allows for very convenient loading of code for interactive work, while giving each program a 'clean sheet' to run in. + Arguments are expanded using shell-like glob match. Patterns + '*', '?', '[seq]' and '[!seq]' can be used. Additionally, + tilde '~' will be expanded into user's home directory. Unlike + real shells, quotation does not suppress expansions. Use + *two* back slashes (e.g., '\\\\*') to suppress expansions. + To completely disable these expansions, you can use -G flag. + Options: -n: __name__ is NOT set to '__main__', but to the running file's name @@ -439,10 +447,13 @@ python-profiler package from non-free.""") will run the example module. + -G: disable shell-like glob expansion of arguments. + """ # get arguments and set sys.argv for program to be run. - opts, arg_lst = self.parse_options(parameter_s, 'nidtN:b:pD:l:rs:T:em:', + opts, arg_lst = self.parse_options(parameter_s, + 'nidtN:b:pD:l:rs:T:em:G', mode='list', list_all=1) if "m" in opts: modulename = opts["m"][0] @@ -476,8 +487,11 @@ python-profiler package from non-free.""") # were run from a system shell. save_argv = sys.argv # save it for later restoring - # simulate shell expansion on arguments, at least tilde expansion - args = [ os.path.expanduser(a) for a in arg_lst[1:] ] + if 'G' in opts: + args = arg_lst[1:] + else: + # tilde and glob expansion + args = shellglob(map(os.path.expanduser, arg_lst[1:])) sys.argv = [filename] + args # put in the proper filename # protect sys.argv from potential unicode strings on Python 2: diff --git a/IPython/core/tests/print_argv.py b/IPython/core/tests/print_argv.py new file mode 100644 index 0000000..8d99000 --- /dev/null +++ b/IPython/core/tests/print_argv.py @@ -0,0 +1,2 @@ +import sys +print sys.argv[1:] diff --git a/IPython/core/tests/test_run.py b/IPython/core/tests/test_run.py index 26558fe..dfc85d7 100644 --- a/IPython/core/tests/test_run.py +++ b/IPython/core/tests/test_run.py @@ -86,6 +86,28 @@ def doctest_run_builtins(): ....: """ + +def doctest_run_option_parser(): + r"""Test option parser in %run. + + In [1]: %run print_argv.py + [] + + In [2]: %run print_argv.py print*.py + ['print_argv.py'] + + In [3]: %run print_argv.py print\\*.py + ['print*.py'] + + In [4]: %run print_argv.py 'print*.py' + ['print_argv.py'] + + In [5]: %run -G print_argv.py print*.py + ['print*.py'] + + """ + + @py3compat.doctest_refactor_print def doctest_reset_del(): """Test that resetting doesn't cause errors in __del__ methods. diff --git a/IPython/utils/path.py b/IPython/utils/path.py index 79cff4c..137f930 100644 --- a/IPython/utils/path.py +++ b/IPython/utils/path.py @@ -19,6 +19,7 @@ import sys import tempfile import warnings from hashlib import md5 +import glob import IPython from IPython.testing.skipdoctest import skip_doctest @@ -355,6 +356,28 @@ def expand_path(s): return s +def unescape_glob(string): + """Unescape glob pattern in `string`.""" + def unescape(s): + for pattern in '*[]!?': + s = s.replace(r'\{0}'.format(pattern), pattern) + return s + return '\\'.join(map(unescape, string.split('\\\\'))) + + +def shellglob(args): + """ + Do glob expansion for each element in `args` and return a flattened list. + + Unmatched glob pattern will remain as-is in the returned list. + + """ + expanded = [] + for a in args: + expanded.extend(glob.glob(a) or [unescape_glob(a)]) + return expanded + + def target_outdated(target,deps): """Determine whether a target is out of date. diff --git a/IPython/utils/tests/test_path.py b/IPython/utils/tests/test_path.py index 8e7fa49..6f15d57 100644 --- a/IPython/utils/tests/test_path.py +++ b/IPython/utils/tests/test_path.py @@ -32,6 +32,7 @@ from IPython.testing.decorators import skip_if_not_win32, skip_win32 from IPython.testing.tools import make_tempfile, AssertPrints from IPython.utils import path, io from IPython.utils import py3compat +from IPython.utils.tempdir import TemporaryDirectory # Platform-dependent imports try: @@ -444,3 +445,48 @@ def test_unicode_in_filename(): path.get_py_filename(u'fooéè.py', force_win32=False) except IOError as ex: str(ex) + + +def test_shellglob(): + """Test glob expansion for %run magic.""" + filenames_start_with_a = map('a{0}'.format, range(3)) + filenames_end_with_b = map('{0}b'.format, range(3)) + filenames = filenames_start_with_a + filenames_end_with_b + + with TemporaryDirectory() as td: + save = os.getcwdu() + try: + os.chdir(td) + + # Create empty files + for fname in filenames: + open(os.path.join(td, fname), 'w').close() + + def assert_match(patterns, matches): + # glob returns unordered list. that's why sorted is required. + nt.assert_equals(sorted(path.shellglob(patterns)), + sorted(matches)) + + assert_match(['*'], filenames) + assert_match(['a*'], filenames_start_with_a) + assert_match(['*c'], ['*c']) + assert_match(['*', 'a*', '*b', '*c'], + filenames + + filenames_start_with_a + + filenames_end_with_b + + ['*c']) + + assert_match([r'\*'], ['*']) + assert_match([r'a\*', 'a*'], ['a*'] + filenames_start_with_a) + assert_match(['a[012]'], filenames_start_with_a) + assert_match([r'a\[012]'], ['a[012]']) + finally: + os.chdir(save) + + +def test_unescape_glob(): + nt.assert_equals(path.unescape_glob(r'\*\[\!\]\?'), '*[!]?') + nt.assert_equals(path.unescape_glob(r'\\*'), r'\*') + nt.assert_equals(path.unescape_glob(r'\\\*'), r'\*') + nt.assert_equals(path.unescape_glob(r'\\a'), r'\a') + nt.assert_equals(path.unescape_glob(r'\a'), r'\a')