Skip to content

Commit 73dba5a

Browse files
committed
echo: Add echo.py and test script
1 parent 03a6c1a commit 73dba5a

File tree

2 files changed

+142
-0
lines changed

2 files changed

+142
-0
lines changed

src/echo.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#!/usr/bin/python3
2+
3+
import codecs
4+
import re
5+
from optparse import OptionParser, BadOptionError, AmbiguousOptionError
6+
7+
ESCAPES_PATTERN = re.compile(
8+
r"(\\0[0-7]{1,3}|\\x[0-9A-Za-z]{1,2}|\\[\\0abcefnrtv])",
9+
re.UNICODE | re.VERBOSE,
10+
)
11+
12+
13+
class PassthroughOptionParser(OptionParser):
14+
"""
15+
A modified version of OptionParser that treats unknown options and "--" as
16+
regular arguments. Always behaves as if interspersed args are disabled.
17+
"""
18+
19+
def _process_args(self, largs, rargs, values):
20+
parsing_options = True
21+
22+
for arg in rargs:
23+
if parsing_options and arg and arg[0] == "-" and arg != "--":
24+
try:
25+
super()._process_args([], [arg], values)
26+
except (BadOptionError, AmbiguousOptionError) as e:
27+
parsing_options = False
28+
largs.append(e.opt_str)
29+
else:
30+
parsing_options = False
31+
largs.append(arg)
32+
33+
rargs.clear()
34+
35+
36+
def echo(opts, args):
37+
string = " ".join(args)
38+
39+
if opts.escapes:
40+
41+
def decode_match(match: re.Match[str]) -> str:
42+
try:
43+
if (escape := match.group(0))[1] == "0" and len(escape) > 2:
44+
# Convert octal escapes from "\0NNN" to Python's form
45+
# ("\NNN" without the "0").
46+
escape = "\\" + escape[2:]
47+
48+
return codecs.decode(escape, "unicode_escape")
49+
except UnicodeDecodeError:
50+
return match.group(0)
51+
52+
string = ESCAPES_PATTERN.sub(decode_match, string)
53+
54+
print(string, end="" if opts.n else "\n")
55+
56+
57+
if __name__ == "__main__":
58+
parser = PassthroughOptionParser(
59+
usage="Usage: %prog [OPTION]... [STRING]...",
60+
description="Print STRING(s) to standard output.",
61+
add_help_option=False,
62+
)
63+
parser.disable_interspersed_args()
64+
parser.add_option("--help", action="help", help="show usage information and exit")
65+
66+
parser.add_option(
67+
"-n", action="store_true", help="do not output the trailing newline"
68+
)
69+
parser.add_option(
70+
"-e",
71+
dest="escapes",
72+
action="store_true",
73+
help="enable interpretation of backslash escapes",
74+
)
75+
parser.add_option(
76+
"-E",
77+
dest="escapes",
78+
action="store_false",
79+
default=False,
80+
help="disable interpretation of backslash escapes (default)",
81+
)
82+
83+
echo(*parser.parse_args())

tests/echo.sh

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/env sh
2+
3+
set -eux
4+
5+
## basic echo
6+
7+
result="$(./echo.py foo bar)"
8+
9+
test "${result}" = 'foo bar'
10+
11+
## double hyphen
12+
13+
result="$(./echo.py -- foo)"
14+
15+
test "${result}" = '-- foo'
16+
17+
## multiple double hyphens
18+
19+
result="$(./echo.py -- foo --)"
20+
21+
test "${result}" = '-- foo --'
22+
23+
## unknown option
24+
25+
result="$(./echo.py -x foo)"
26+
27+
test "${result}" = '-x foo'
28+
29+
## unknown option and double hyphen
30+
31+
result="$(./echo.py -x -- foo)"
32+
33+
test "${result}" = '-x -- foo'
34+
35+
## escape codes
36+
37+
result="$(./echo.py -e 'foo \x41' '\0102')"
38+
39+
test "${result}" = 'foo A B'
40+
41+
## no arguments
42+
43+
result="$(./echo.py)"
44+
45+
test "${result}" = "$(printf '\n')"
46+
47+
## empty arguments
48+
49+
result="$(./echo.py '' foo '' bar '' '')"
50+
51+
test "${result}" = ' foo bar '
52+
53+
## no trailing newline
54+
55+
n_lines="$(./echo.py -n 'foo' | wc -l)"
56+
57+
test "${n_lines}" = 0
58+
59+
exit 0

0 commit comments

Comments
 (0)