Skip to content

Commit 73d021f

Browse files
authored
fix: correctly split quoted args (#909)
1 parent 62b2fd0 commit 73d021f

12 files changed

+159
-36
lines changed

docs/jq.md

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/strings.md

+26
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/jq.bzl

+1-1
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ def jq(name, srcs, filter = None, filter_file = None, args = [], out = None, dat
144144
145145
filter_file: File containing filter expression (alternative to `filter`)
146146
args: Additional args to pass to jq
147-
expand_args: Run bazel's location-expansion on the args.
147+
expand_args: Run bazel's location and make variable expansion on the args.
148148
out: Name of the output json file; defaults to the rule name plus ".json"
149149
**kwargs: Other common named parameters such as `tags` or `visibility`
150150
"""

lib/private/BUILD.bazel

+7
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ bzl_library(
160160
srcs = ["jq.bzl"],
161161
visibility = ["//lib:__subpackages__"],
162162
deps = [
163+
":expand_variables",
164+
":strings",
163165
"//lib:stamping",
164166
],
165167
)
@@ -182,6 +184,10 @@ bzl_library(
182184
name = "params_file",
183185
srcs = ["params_file.bzl"],
184186
visibility = ["//lib:__subpackages__"],
187+
deps = [
188+
":expand_variables",
189+
":strings",
190+
],
185191
)
186192

187193
bzl_library(
@@ -204,6 +210,7 @@ bzl_library(
204210
visibility = ["//lib:__subpackages__"],
205211
deps = [
206212
":expand_variables",
213+
":strings",
207214
"//lib:stamping",
208215
"@bazel_skylib//lib:dicts",
209216
],

lib/private/bats.bzl

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def _bats_test_impl(ctx):
4141
for (key, value) in ctx.attr.env.items():
4242
envs.append(_ENV_SET.format(
4343
key = key,
44-
value = " ".join([expand_variables(ctx, exp, attribute_name = "env") for exp in ctx.expand_location(value, targets = ctx.attr.data).split(" ")]),
44+
value = expand_variables(ctx, ctx.expand_location(value, targets = ctx.attr.data), attribute_name = "env"),
4545
))
4646

4747
# See https://www.msys2.org/wiki/Porting/:

lib/private/expand_template.bzl

+2-5
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,12 @@
22

33
load("@bazel_skylib//lib:dicts.bzl", "dicts")
44
load("//lib:stamping.bzl", "STAMP_ATTRS", "maybe_stamp")
5-
load(":expand_variables.bzl", _expand_variables = "expand_variables")
5+
load(":expand_variables.bzl", "expand_variables")
66

77
def _expand_substitutions(ctx, output, substitutions):
88
result = {}
99
for k, v in substitutions.items():
10-
result[k] = " ".join([
11-
_expand_variables(ctx, e, outs = [output], attribute_name = "substitutions")
12-
for e in ctx.expand_location(v, targets = ctx.attr.data).split(" ")
13-
])
10+
result[k] = expand_variables(ctx, ctx.expand_location(v, targets = ctx.attr.data), outs = [output], attribute_name = "substitutions")
1411
return result
1512

1613
def _expand_template_impl(ctx):

lib/private/jq.bzl

+12-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""Implementation for jq rule"""
22

33
load("//lib:stamping.bzl", "STAMP_ATTRS", "maybe_stamp")
4+
load(":expand_variables.bzl", "expand_variables")
5+
load(":strings.bzl", "split_args")
46

57
_jq_attrs = dict({
68
"srcs": attr.label_list(
@@ -22,20 +24,14 @@ _jq_attrs = dict({
2224
),
2325
}, **STAMP_ATTRS)
2426

25-
def _expand_locations(ctx, s):
26-
# `.split(" ")` is a work-around https://github.com/bazelbuild/bazel/issues/10309
27-
# TODO: If the string has intentional spaces or if one or more of the expanded file
28-
# locations has a space in the name, we will incorrectly split it into multiple arguments
29-
return ctx.expand_location(s, targets = ctx.attr.data).split(" ")
30-
3127
def _jq_impl(ctx):
3228
jq_bin = ctx.toolchains["@aspect_bazel_lib//lib:jq_toolchain_type"].jqinfo.bin
3329

3430
out = ctx.outputs.out or ctx.actions.declare_file(ctx.attr.name + ".json")
3531
if ctx.attr.expand_args:
3632
args = []
3733
for a in ctx.attr.args:
38-
args += _expand_locations(ctx, a)
34+
args += split_args(expand_variables(ctx, ctx.expand_location(a, targets = ctx.attr.data), outs = [out]))
3935
else:
4036
args = ctx.attr.args
4137

@@ -52,7 +48,7 @@ def _jq_impl(ctx):
5248
args = args + ["--null-input"]
5349

5450
if ctx.attr.filter_file:
55-
args = args + ["--from-file '%s'" % ctx.file.filter_file.path]
51+
args = args + ["--from-file", ctx.file.filter_file.path]
5652
inputs.append(ctx.file.filter_file)
5753

5854
stamp = maybe_stamp(ctx)
@@ -76,9 +72,16 @@ def _jq_impl(ctx):
7672

7773
args = args + ["--slurpfile", "STAMP", stamp_json.path]
7874

75+
# quote args that contain spaces
76+
quoted_args = []
77+
for a in args:
78+
if " " in a:
79+
a = "'{}'".format(a)
80+
quoted_args.append(a)
81+
7982
cmd = "{jq} {args} {filter} {sources} > {out}".format(
8083
jq = jq_bin.path,
81-
args = " ".join(args),
84+
args = " ".join(quoted_args),
8285
filter = "'%s'" % ctx.attr.filter if ctx.attr.filter else "",
8386
sources = " ".join(["'%s'" % file.path for file in ctx.files.srcs]),
8487
out = out.path,

lib/private/params_file.bzl

+5-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
"params_file rule"
22

3+
load(":expand_variables.bzl", "expand_variables")
4+
load(":strings.bzl", "split_args")
5+
36
_ATTRS = {
47
"args": attr.string_list(),
58
"data": attr.label_list(allow_files = True),
@@ -11,12 +14,6 @@ _ATTRS = {
1114
"_windows_constraint": attr.label(default = "@platforms//os:windows"),
1215
}
1316

14-
def _expand_locations(ctx, s):
15-
# `.split(" ")` is a work-around https://github.com/bazelbuild/bazel/issues/10309
16-
# TODO: If the string has intentional spaces or if one or more of the expanded file
17-
# locations has a space in the name, we will incorrectly split it into multiple arguments
18-
return ctx.expand_location(s, targets = ctx.attr.data).split(" ")
19-
2017
def _params_file_impl(ctx):
2118
is_windows = ctx.target_platform_has_constraint(ctx.attr._windows_constraint[platform_common.ConstraintValueInfo])
2219

@@ -29,12 +26,9 @@ def _params_file_impl(ctx):
2926

3027
expanded_args = []
3128

32-
# First expand predefined source/output path variables
29+
# Expand predefined source/output path && predefined variables & custom variables
3330
for a in ctx.attr.args:
34-
expanded_args += _expand_locations(ctx, a)
35-
36-
# Next expand predefined variables & custom variables
37-
expanded_args = [ctx.expand_make_variables("args", e, {}) for e in expanded_args]
31+
expanded_args += split_args(expand_variables(ctx, ctx.expand_location(a, targets = ctx.attr.data), outs = [ctx.outputs.out]))
3832

3933
# ctx.actions.write creates a FileWriteAction which uses UTF-8 encoding.
4034
ctx.actions.write(

lib/private/run_binary.bzl

+3-6
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
load("@bazel_skylib//lib:dicts.bzl", "dicts")
1818
load("//lib:stamping.bzl", "STAMP_ATTRS", "maybe_stamp")
1919
load(":expand_variables.bzl", "expand_variables")
20+
load(":strings.bzl", "split_args")
2021

2122
def _run_binary_impl(ctx):
2223
args = ctx.actions.args()
@@ -46,15 +47,11 @@ Possible fixes:
4647
rule_kind = str(ctx.attr.tool.label),
4748
))
4849

49-
# `expand_locations(...).split(" ")` is a work-around https://github.com/bazelbuild/bazel/issues/10309
50-
# _expand_locations returns an array of args to support $(execpaths) expansions.
51-
# TODO: If the string has intentional spaces or if one or more of the expanded file
52-
# locations has a space in the name, we will incorrectly split it into multiple arguments
5350
for a in ctx.attr.args:
54-
args.add_all([expand_variables(ctx, e, outs = outputs) for e in ctx.expand_location(a, targets = ctx.attr.srcs).split(" ")])
51+
args.add_all(split_args(expand_variables(ctx, ctx.expand_location(a, targets = ctx.attr.srcs), outs = outputs)))
5552
envs = {}
5653
for k, v in ctx.attr.env.items():
57-
envs[k] = " ".join([expand_variables(ctx, e, outs = outputs, attribute_name = "env") for e in ctx.expand_location(v, targets = ctx.attr.srcs).split(" ")])
54+
envs[k] = expand_variables(ctx, ctx.expand_location(v, targets = ctx.attr.srcs), outs = outputs, attribute_name = "env")
5855

5956
stamp = maybe_stamp(ctx)
6057
if stamp:

lib/private/strings.bzl

+70
Original file line numberDiff line numberDiff line change
@@ -583,3 +583,73 @@ def hex(number):
583583
hex_string = "0"
584584

585585
return "{}0x{}".format("-" if is_signed else "", hex_string)
586+
587+
def split_args(s):
588+
"""Split a string into a list space separated arguments
589+
590+
Unlike the naive `.split(" ")`, this function takes quoted strings
591+
and escapes into account.
592+
593+
Args:
594+
s: input string
595+
596+
Returns:
597+
list of strings with each an argument found in the input string
598+
"""
599+
args = []
600+
arg = ""
601+
single_quote = False
602+
double_quote = False
603+
escape = False
604+
for c in s.elems():
605+
if c == "\\":
606+
escape = True
607+
continue
608+
if escape:
609+
# this is an escaped character
610+
if c == " ":
611+
# a dangling escape is not an escape, put the backslack back
612+
arg = arg + "\\"
613+
else:
614+
escape = False
615+
else:
616+
# not an escaped character, look for quotes & spaces
617+
if c == "'":
618+
# single quote char
619+
if double_quote:
620+
# we're in a double quote so single quotes are just chars
621+
pass
622+
elif single_quote:
623+
# end of single quote
624+
single_quote = False
625+
continue
626+
else:
627+
# start of single quote
628+
single_quote = True
629+
continue
630+
elif c == "\"":
631+
# double quote char
632+
if single_quote:
633+
# we're in a single quote so double quotes are just chars
634+
pass
635+
elif double_quote:
636+
# end of double quote
637+
double_quote = False
638+
continue
639+
else:
640+
# start of double quote
641+
double_quote = True
642+
continue
643+
if c == " ":
644+
if not single_quote and not double_quote:
645+
# splitting space
646+
if arg != "":
647+
args.append(arg)
648+
arg = ""
649+
continue
650+
arg = arg + c
651+
652+
# final arg?
653+
if arg != "":
654+
args.append(arg)
655+
return args

lib/strings.bzl

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"Utilities for strings"
22

3-
load("//lib/private:strings.bzl", _chr = "chr", _hex = "hex", _ord = "ord")
3+
load("//lib/private:strings.bzl", _chr = "chr", _hex = "hex", _ord = "ord", _split_args = "split_args")
44

55
chr = _chr
66
ord = _ord
77
hex = _hex
8+
split_args = _split_args

lib/tests/strings_tests.bzl

+29-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
load("@bazel_skylib//lib:partial.bzl", "partial")
44
load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest")
5-
load("//lib/private:strings.bzl", "chr", "hex", "ord")
5+
load("//lib/private:strings.bzl", "chr", "hex", "ord", "split_args")
66

77
def _ord_test_impl(ctx):
88
env = unittest.begin(ctx)
@@ -56,10 +56,38 @@ def _hex_test_impl(ctx):
5656

5757
hex_test = unittest.make(_hex_test_impl)
5858

59+
def _split_args_test_impl(ctx):
60+
env = unittest.begin(ctx)
61+
62+
asserts.equals(env, ["a", "b", "c", "d"], split_args("a b c d"))
63+
64+
# sinle quotes
65+
asserts.equals(env, ["a", "b c", "d"], split_args("a 'b c' d"))
66+
67+
# double quotes
68+
asserts.equals(env, ["a", "b c", "d"], split_args("a \"b c\" d"))
69+
70+
# escaped single quotes
71+
asserts.equals(env, ["a", "'b", "c'", "d"], split_args("a \\'b c\\' d"))
72+
73+
# escaped double quotes
74+
asserts.equals(env, ["a", "\"b", "c\"", "d"], split_args("a \\\"b c\\\" d"))
75+
76+
# sinle quotes containing escaped quotes
77+
asserts.equals(env, ["a", "b'\" c", "d"], split_args("a 'b\\'\\\" c' d"))
78+
79+
# double quotes containing escaped quotes
80+
asserts.equals(env, ["a", "b'\" c", "d"], split_args("a \"b\\'\\\" c\" d"))
81+
82+
return unittest.end(env)
83+
84+
split_args_test = unittest.make(_split_args_test_impl)
85+
5986
def strings_test_suite():
6087
unittest.suite(
6188
"strings_tests",
6289
partial.make(ord_test, timeout = "short"),
6390
partial.make(chr_test, timeout = "short"),
6491
partial.make(hex_test, timeout = "short"),
92+
partial.make(split_args_test, timeout = "short"),
6593
)

0 commit comments

Comments
 (0)