Fix mlr -s shebang doc and reject arrays/maps in contains/index (#1658) (#2058)

The shebang example `#!/usr/bin/env mlr -s` does not work on Linux
because env(1) does not accept arguments to the interpreter; use
`#!/usr/bin/env -S mlr -s` instead.

`contains` and `index` previously stringified their inputs silently,
so `contains([1,2], $foo)` would match against the literal string
`[1, 2]` and produce surprising results. Both now return a type-error
when either argument is an array or map; help text points users at
`any(arr, func(e){return e == x})` for membership tests.
This commit is contained in:
John Kerl 2026-05-17 11:40:39 -04:00 committed by GitHub
parent e0cf596853
commit 1bb9ba12c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 32 additions and 8 deletions

View file

@ -1,4 +1,4 @@
#!/usr/bin/env mlr -s
#!/usr/bin/env -S mlr -s
--c2p
filter '$quantity != 20' # Here is a comment
then count-distinct -f shape

View file

@ -61,6 +61,7 @@ Flags:
mlr help format-conversion-keystroke-saver-flags
mlr help json-only-flags
mlr help legacy-flags
mlr help markdown-only-flags
mlr help miscellaneous-flags
mlr help output-colorization-flags
mlr help pprint-only-flags

View file

@ -1217,12 +1217,13 @@ collapse_whitespace (class=string #args=1) Strip repeated whitespace from strin
### contains
<pre class="pre-non-highlight-non-pair">
contains (class=string #args=2) Returns true if the first argument contains the second as a substring. This is like saying `index(arg1, arg2) >= 0`but with less keystroking.
contains (class=string #args=2) Returns true if the first argument contains the second as a substring. This is like saying `index(arg1, arg2) >= 0` but with less keystroking. Stringifies non-string scalar inputs; raises an error if either argument is an array or map. To test for array membership, use `any`, e.g. `any([1,2,3], func(e) {return e == $foo})`.
Examples:
contains("abcde", "e") gives true
contains("abcde", "x") gives false
contains(12345, 34) gives true
contains("forêt", "ê") gives true
contains([1,2,3], 2) gives (error)
</pre>
@ -1258,12 +1259,13 @@ gsub("prefix4529:suffix8567", "(....ix)([0-9]+)", "[\1 : \2]") gives "[prefix :
### index
<pre class="pre-non-highlight-non-pair">
index (class=string #args=2) Returns the index (1-based) of the second argument within the first. Returns -1 if the second argument isn't a substring of the first. Stringifies non-string inputs. Uses UTF-8 encoding to count characters, not bytes.
index (class=string #args=2) Returns the index (1-based) of the second argument within the first. Returns -1 if the second argument isn't a substring of the first. Stringifies non-string scalar inputs; raises an error if either argument is an array or map. Uses UTF-8 encoding to count characters, not bytes.
Examples:
index("abcde", "e") gives 5
index("abcde", "x") gives -1
index(12345, 34) gives 3
index("forêt", "t") gives 5
index([1,2,3], 2) gives (error)
</pre>

View file

@ -35,7 +35,7 @@ circle 3 0.3
Typing this out can get a bit old, if the only thing that changes for you is the filename. Some options include:
* On Linux/Mac/etc you can make a script with `#!/bin/sh` which invokes Miller as part of the shell-script body.
* On Linux/Mac/etc you can make a script with `#!/usr/bin/env mlr -s` which invokes Miller.
* On Linux/Mac/etc you can make a script with `#!/usr/bin/env -S mlr -s` which invokes Miller.
* On any platform you can put the reusable part of your command line into a text file (say `myflags.txt`), then `mlr -s myflags-txt filename-which-varies.csv`.
Let's look at examples of each.
@ -135,7 +135,7 @@ Here instead of putting `#!/bin/bash` on the first line, we can put `mlr` direct
<b>cat ./example-mlr-s-script</b>
</pre>
<pre class="pre-non-highlight-in-pair">
#!/usr/bin/env mlr -s
#!/usr/bin/env -S mlr -s
--c2p
filter '$quantity != 20' # Here is a comment
then count-distinct -f shape

View file

@ -13,7 +13,7 @@ GENMD-EOF
Typing this out can get a bit old, if the only thing that changes for you is the filename. Some options include:
* On Linux/Mac/etc you can make a script with `#!/bin/sh` which invokes Miller as part of the shell-script body.
* On Linux/Mac/etc you can make a script with `#!/usr/bin/env mlr -s` which invokes Miller.
* On Linux/Mac/etc you can make a script with `#!/usr/bin/env -S mlr -s` which invokes Miller.
* On any platform you can put the reusable part of your command line into a text file (say `myflags.txt`), then `mlr -s myflags-txt filename-which-varies.csv`.
Let's look at examples of each.

View file

@ -134,6 +134,9 @@ func BIF_index(input1, input2 *mlrval.Mlrval) *mlrval.Mlrval {
if input1.IsError() {
return mlrval.FromTypeErrorUnary("index", input1)
}
if input1.IsArrayOrMap() || input2.IsArrayOrMap() {
return mlrval.FromTypeErrorBinary("index", input1, input2)
}
sinput1 := input1.String()
sinput2 := input2.String()
@ -156,6 +159,9 @@ func BIF_contains(input1, input2 *mlrval.Mlrval) *mlrval.Mlrval {
if input1.IsError() {
return input1
}
if input1.IsArrayOrMap() || input2.IsArrayOrMap() {
return mlrval.FromTypeErrorBinary("contains", input1, input2)
}
return mlrval.FromBool(strings.Contains(input1.String(), input2.String()))
}

View file

@ -581,25 +581,27 @@ Arrays are new in Miller 6; the substr function is older.`,
{
name: "index",
class: FUNC_CLASS_STRING,
help: `Returns the index (1-based) of the second argument within the first. Returns -1 if the second argument isn't a substring of the first. Stringifies non-string inputs. Uses UTF-8 encoding to count characters, not bytes.`,
help: `Returns the index (1-based) of the second argument within the first. Returns -1 if the second argument isn't a substring of the first. Stringifies non-string scalar inputs; raises an error if either argument is an array or map. Uses UTF-8 encoding to count characters, not bytes.`,
binaryFunc: bifs.BIF_index,
examples: []string{
`index("abcde", "e") gives 5`,
`index("abcde", "x") gives -1`,
`index(12345, 34) gives 3`,
`index("forêt", "t") gives 5`,
`index([1,2,3], 2) gives (error)`,
},
},
{
name: "contains",
class: FUNC_CLASS_STRING,
help: `Returns true if the first argument contains the second as a substring. This is like saying ` + "`index(arg1, arg2) >= 0`" + `but with less keystroking.`,
help: `Returns true if the first argument contains the second as a substring. This is like saying ` + "`index(arg1, arg2) >= 0`" + ` but with less keystroking. Stringifies non-string scalar inputs; raises an error if either argument is an array or map. To test for array membership, use ` + "`any`" + `, e.g. ` + "`any([1,2,3], func(e) {return e == $foo})`" + `.`,
binaryFunc: bifs.BIF_contains,
examples: []string{
`contains("abcde", "e") gives true`,
`contains("abcde", "x") gives false`,
`contains(12345, 34) gives true`,
`contains("forêt", "ê") gives true`,
`contains([1,2,3], 2) gives (error)`,
},
},

View file

@ -0,0 +1 @@
mlr -n put -q -f ${CASEDIR}/mlr

View file

View file

@ -0,0 +1,5 @@
(error)
(error)
(error)
(error)
(error)

View file

@ -0,0 +1,7 @@
end {
print contains([1,2,3], 2);
print contains("abc", [1,2,3]);
print contains({"a":1}, "a");
print index([1,2,3], 2);
print index("abc", {"k":1});
}