Skip to content

Commit

Permalink
Merge pull request #2165 from tkf/run-glob
Browse files Browse the repository at this point in the history
Expand globs (i.e., '*' and '?') in `%run`.  Use `-G` to skip.
  • Loading branch information
bfroehle committed Oct 7, 2012
2 parents 097ce42 + 2446f12 commit e54a60b
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 5 deletions.
24 changes: 19 additions & 5 deletions IPython/core/magics/execution.py
Expand Up @@ -44,10 +44,11 @@
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
#-----------------------------------------------------------------------------
Expand Down Expand Up @@ -324,7 +325,7 @@ def run(self, parameter_s='', runner=None,
"""Run the named file inside IPython as a program.
Usage:\\
%run [-n -i -t [-N<N>] -d [-b<N>] -p [profile options]] file [args]
%run [-n -i -t [-N<N>] -d [-b<N>] -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
Expand All @@ -345,6 +346,13 @@ def run(self, parameter_s='', runner=None,
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
Expand Down Expand Up @@ -439,10 +447,13 @@ def run(self, parameter_s='', runner=None,
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]
Expand Down Expand Up @@ -476,8 +487,11 @@ def run(self, parameter_s='', runner=None,
# 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:
Expand Down
2 changes: 2 additions & 0 deletions IPython/core/tests/print_argv.py
@@ -0,0 +1,2 @@
import sys
print sys.argv[1:]
22 changes: 22 additions & 0 deletions IPython/core/tests/test_run.py
Expand Up @@ -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.
Expand Down
23 changes: 23 additions & 0 deletions IPython/utils/path.py
Expand Up @@ -19,6 +19,7 @@
import tempfile
import warnings
from hashlib import md5
import glob

import IPython
from IPython.testing.skipdoctest import skip_doctest
Expand Down Expand Up @@ -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.
Expand Down
46 changes: 46 additions & 0 deletions IPython/utils/tests/test_path.py
Expand Up @@ -32,6 +32,7 @@
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:
Expand Down Expand Up @@ -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')

0 comments on commit e54a60b

Please sign in to comment.