Skip to content

Commit 816d02c

Browse files
Patrik Kopkanvstinner
authored andcommitted
00335: Backport pathfix change
Tools/scripts/pathfix.py backports Add -k and -a command line options to preserve and add shebang flags In upstream since 3.8: https://bugs.python.org/issue37064 Assume all .py files are Python scripts when working recursively: In upstream since 3.8: https://bugs.python.org/issue38347 Co-authored-by: Victor Stinner <vstinner@redhat.com>
1 parent 155d2c8 commit 816d02c

3 files changed

Lines changed: 186 additions & 5 deletions

File tree

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import os
2+
import subprocess
3+
import sys
4+
import unittest
5+
from test import support
6+
from test.test_tools import import_tool, scriptsdir, skip_if_missing
7+
8+
9+
# need Tools/script/ directory: skip if run on Python installed on the system
10+
skip_if_missing()
11+
12+
13+
class TestPathfixFunctional(unittest.TestCase):
14+
script = os.path.join(scriptsdir, 'pathfix.py')
15+
16+
def setUp(self):
17+
self.addCleanup(support.unlink, support.TESTFN)
18+
19+
def pathfix(self, shebang, pathfix_flags, exitcode=0, stdout='', stderr='',
20+
directory=''):
21+
if directory:
22+
# bpo-38347: Test filename should contain lowercase, uppercase,
23+
# "-", "_" and digits.
24+
filename = os.path.join(directory, 'script-A_1.py')
25+
pathfix_arg = directory
26+
else:
27+
filename = support.TESTFN
28+
pathfix_arg = filename
29+
30+
with open(filename, 'w', encoding='utf8') as f:
31+
f.write(f'{shebang}\n' + 'print("Hello world")\n')
32+
33+
proc = subprocess.run(
34+
[sys.executable, self.script,
35+
*pathfix_flags, '-n', pathfix_arg],
36+
capture_output=True, text=1)
37+
38+
if stdout == '' and proc.returncode == 0:
39+
stdout = f'{filename}: updating\n'
40+
self.assertEqual(proc.returncode, exitcode, proc)
41+
self.assertEqual(proc.stdout, stdout, proc)
42+
self.assertEqual(proc.stderr, stderr, proc)
43+
44+
with open(filename, 'r', encoding='utf8') as f:
45+
output = f.read()
46+
47+
lines = output.split('\n')
48+
self.assertEqual(lines[1:], ['print("Hello world")', ''])
49+
new_shebang = lines[0]
50+
51+
if proc.returncode != 0:
52+
self.assertEqual(shebang, new_shebang)
53+
54+
return new_shebang
55+
56+
def test_recursive(self):
57+
tmpdir = support.TESTFN + '.d'
58+
self.addCleanup(support.rmtree, tmpdir)
59+
os.mkdir(tmpdir)
60+
expected_stderr = f"recursedown('{os.path.basename(tmpdir)}')\n"
61+
self.assertEqual(
62+
self.pathfix(
63+
'#! /usr/bin/env python',
64+
['-i', '/usr/bin/python3'],
65+
directory=tmpdir,
66+
stderr=expected_stderr),
67+
'#! /usr/bin/python3')
68+
69+
def test_pathfix(self):
70+
self.assertEqual(
71+
self.pathfix(
72+
'#! /usr/bin/env python',
73+
['-i', '/usr/bin/python3']),
74+
'#! /usr/bin/python3')
75+
self.assertEqual(
76+
self.pathfix(
77+
'#! /usr/bin/env python -R',
78+
['-i', '/usr/bin/python3']),
79+
'#! /usr/bin/python3')
80+
81+
def test_pathfix_keeping_flags(self):
82+
self.assertEqual(
83+
self.pathfix(
84+
'#! /usr/bin/env python -R',
85+
['-i', '/usr/bin/python3', '-k']),
86+
'#! /usr/bin/python3 -R')
87+
self.assertEqual(
88+
self.pathfix(
89+
'#! /usr/bin/env python',
90+
['-i', '/usr/bin/python3', '-k']),
91+
'#! /usr/bin/python3')
92+
93+
def test_pathfix_adding_flag(self):
94+
self.assertEqual(
95+
self.pathfix(
96+
'#! /usr/bin/env python',
97+
['-i', '/usr/bin/python3', '-a', 's']),
98+
'#! /usr/bin/python3 -s')
99+
self.assertEqual(
100+
self.pathfix(
101+
'#! /usr/bin/env python -S',
102+
['-i', '/usr/bin/python3', '-a', 's']),
103+
'#! /usr/bin/python3 -s')
104+
self.assertEqual(
105+
self.pathfix(
106+
'#! /usr/bin/env python -V',
107+
['-i', '/usr/bin/python3', '-a', 'v', '-k']),
108+
'#! /usr/bin/python3 -vV')
109+
self.assertEqual(
110+
self.pathfix(
111+
'#! /usr/bin/env python',
112+
['-i', '/usr/bin/python3', '-a', 'Rs']),
113+
'#! /usr/bin/python3 -Rs')
114+
self.assertEqual(
115+
self.pathfix(
116+
'#! /usr/bin/env python -W default',
117+
['-i', '/usr/bin/python3', '-a', 's', '-k']),
118+
'#! /usr/bin/python3 -sW default')
119+
120+
def test_pathfix_adding_errors(self):
121+
self.pathfix(
122+
'#! /usr/bin/env python -E',
123+
['-i', '/usr/bin/python3', '-a', 'W default', '-k'],
124+
exitcode=2,
125+
stderr="-a option doesn't support whitespaces")
126+
127+
128+
if __name__ == '__main__':
129+
unittest.main()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add option -k to pathscript.py script: preserve shebang flags.
2+
Add option -a to pathscript.py script: add flags.

Tools/scripts/pathfix.py

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env python3
22

3-
# Change the #! line occurring in Python scripts. The new interpreter
3+
# Change the #! line (shebang) occurring in Python scripts. The new interpreter
44
# pathname must be given with a -i option.
55
#
66
# Command line arguments are files or directories to be processed.
@@ -10,7 +10,13 @@
1010
# arguments).
1111
# The original file is kept as a back-up (with a "~" attached to its name),
1212
# -n flag can be used to disable this.
13-
#
13+
14+
# Sometimes you may find shebangs with flags such as `#! /usr/bin/env python -si`.
15+
# Normally, pathfix overwrites the entire line, including the flags.
16+
# To change interpreter and keep flags from the original shebang line, use -k.
17+
# If you want to keep flags and add to them one single literal flag, use option -a.
18+
19+
1420
# Undoubtedly you can do this using find and sed or perl, but this is
1521
# a nice example of Python code that recurses down a directory tree
1622
# and uses regular expressions. Also note several subtleties like
@@ -33,16 +39,21 @@
3339
new_interpreter = None
3440
preserve_timestamps = False
3541
create_backup = True
42+
keep_flags = False
43+
add_flags = b''
3644

3745

3846
def main():
3947
global new_interpreter
4048
global preserve_timestamps
4149
global create_backup
42-
usage = ('usage: %s -i /interpreter -p -n file-or-directory ...\n' %
50+
global keep_flags
51+
global add_flags
52+
53+
usage = ('usage: %s -i /interpreter -p -n -k -a file-or-directory ...\n' %
4354
sys.argv[0])
4455
try:
45-
opts, args = getopt.getopt(sys.argv[1:], 'i:pn')
56+
opts, args = getopt.getopt(sys.argv[1:], 'i:a:kpn')
4657
except getopt.error as msg:
4758
err(str(msg) + '\n')
4859
err(usage)
@@ -54,6 +65,13 @@ def main():
5465
preserve_timestamps = True
5566
if o == '-n':
5667
create_backup = False
68+
if o == '-k':
69+
keep_flags = True
70+
if o == '-a':
71+
add_flags = a.encode()
72+
if b' ' in add_flags:
73+
err("-a option doesn't support whitespaces")
74+
sys.exit(2)
5775
if not new_interpreter or not new_interpreter.startswith(b'/') or \
5876
not args:
5977
err('-i option or file-or-directory missing\n')
@@ -96,6 +114,7 @@ def recursedown(dirname):
96114
if recursedown(fullname): bad = 1
97115
return bad
98116

117+
99118
def fix(filename):
100119
## dbg('fix(%r)\n' % (filename,))
101120
try:
@@ -166,12 +185,43 @@ def fix(filename):
166185
# Return success
167186
return 0
168187

188+
189+
def parse_shebang(shebangline):
190+
shebangline = shebangline.rstrip(b'\n')
191+
start = shebangline.find(b' -')
192+
if start == -1:
193+
return b''
194+
return shebangline[start:]
195+
196+
197+
def populate_flags(shebangline):
198+
old_flags = b''
199+
if keep_flags:
200+
old_flags = parse_shebang(shebangline)
201+
if old_flags:
202+
old_flags = old_flags[2:]
203+
if not (old_flags or add_flags):
204+
return b''
205+
# On Linux, the entire string following the interpreter name
206+
# is passed as a single argument to the interpreter.
207+
# e.g. "#! /usr/bin/python3 -W Error -s" runs "/usr/bin/python3 "-W Error -s"
208+
# so shebang should have single '-' where flags are given and
209+
# flag might need argument for that reasons adding new flags is
210+
# between '-' and original flags
211+
# e.g. #! /usr/bin/python3 -sW Error
212+
return b' -' + add_flags + old_flags
213+
214+
169215
def fixline(line):
170216
if not line.startswith(b'#!'):
171217
return line
218+
172219
if b"python" not in line:
173220
return line
174-
return b'#! ' + new_interpreter + b'\n'
221+
222+
flags = populate_flags(line)
223+
return b'#! ' + new_interpreter + flags + b'\n'
224+
175225

176226
if __name__ == '__main__':
177227
main()

0 commit comments

Comments
 (0)