9
\$\begingroup\$

I'm trying to shorten a bash solution to a Code Golf challenge, which requires an output of true or false. I've managed to condense the key piece of the code down to a substring check, to determine if the provided argument, $1, is a substring of a particular hard-coded fixed string, of the form:

[[ FIXEDSTRING = *$1* ]]&&echo true||echo false

# And a minor improvement to shave a character on the test:
[[ FIXEDSTRING =~ $1 ]]&&echo true||echo false

My question is, is there some clever way to shorten this, either in general, or for the specific case of converting success to true and failure to false?

In a language like Python, I'd use:

# Shortest solution without imports I know of
str(sys.argv[1]in"FIXEDSTRING").lower()  # Also feels a little long, kinda
                                         # curious if there is a better way

# Shorter when json is already imported for some other purpose, saves a character (more if
# json aliased or dumps denamespaced via from json import*):
json.dumps(sys.argv[1]in"FIXEDSTRING")

for the specific case of "success is true, failure is false", and one of:

# When string lengths close enough for stepped slicing to work, only 3 characters longer
# than the true/false specific solution
"ftarlusee"[sys.argv[1]in"FIXEDSTRING"::2]

# When string lengths aren't compatible for stepped slicing, 
("false","true")[sys.argv[1]in"FIXEDSTRING"]

when I need to select between arbitrary strings, not just true/false.

But AFAICT:

  1. bash has no concept of converting from success/failure to a specific string (without relying on if/then/fi,case/esac or &&/|| and manually running echo with a literal string for each string option)
  2. bash arrays can't be used "anonymously", and the array indexing and slicing syntax is so verbose you can't save any characters this way (e.g. a=(true false);[[ FIXEDSTRING = *$1* ]];echo ${a[$?]} is six characters longer than my current approach; useful if I needed to convert exit status to true/false in multiple places, but not for a one-off use)
  3. Stepped slicing of a string isn't a thing.
\$\endgroup\$
0

5 Answers 5

5
\$\begingroup\$

Here's a slightly shorter method in pure Bash. It doesn't beat the expr based method, but it doesn't use any utils.

false
[[ FIXEDSTRING =~ $1 ]]&&true
echo $_
\$\endgroup\$
3
  • \$\begingroup\$ Nice! Splits the difference between my naive solution and the expr one (this one is three character shorter, expr is six characters shorter), and as you say, is pure Bash so it definitely works on any setup, even weirdly limited ones without access to coreutils. I'd read the tip to use $_, but hadn't realized how it could help here. Limited to the true/false case for direct use, but combined with : you could make it handle any string for two characters more apiece (though if you need it for both, the naive solution wins again). \$\endgroup\$ Feb 23, 2023 at 23:55
  • \$\begingroup\$ I see that this works, but am curious why true; echo $_ eg returns true? we invoked true with no arguments, after all... \$\endgroup\$
    – Jonah
    Feb 24, 2023 at 5:20
  • 1
    \$\begingroup\$ @Jonah: $_ is the last argument to the prior command. argv-style, the name of the program is the first argument (it's $0 for bash too), so if you give it no arguments, then the program name itself is also the last argument. \$\endgroup\$ Feb 24, 2023 at 13:58
5
\$\begingroup\$

This one is the same size as the expr method while still being pure Bash. It takes advantage of $_ and the fact that true/false are builtins that simply returns the status you would expect.

FIXEDSTRING
${_/*$1*}true||false
echo $_
Explanation:
  • "FIXEDSTRING" gets stored into $_ by attempting to execute it with no args.

  • If $1 is a substring of "FIXEDSTRING", then it will be removed in the pattern replacement and allows Bash to execute the true builtin.

  • Otherwise, the pattern replacement will result in no change to "FIXEDSTRING" if it's not a substring, thus executing FIXEDSTRINGtrue, which is not a valid command and fails through to executing false. Thus putting either "true" or "false" into $_ based on the pattern replacement result.

  • $_ is then echoed out.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Welcome to Code Golf, and nice tip! \$\endgroup\$ Feb 24, 2023 at 17:33
  • \$\begingroup\$ Nice. Sadly won't work for my specific Code Golf evaluator (it checks all text output, including the stderr output you'd get for running the non-existent FIXEDSTRING command and the weird prefixed version of true), but it's likely helpful to other Code Golf evaluation setups. Up-voted! \$\endgroup\$ Feb 25, 2023 at 0:07
3
\$\begingroup\$

I found one way to improve on this using the expr command; not bash itself, but at least a GNU coreutil so it exists pretty universally (and more importantly, on the Code Golf test bed):

expr true \& FIXEDSTRING : .*$1 \| false

FIXEDSTRING : .*$1 is a regex pattern match; it's implicitly anchored with a leading ^, thus the need for the .* in front of $1 (probably would need to be .\*$1 if there was a possibility of a file with a name matching the pattern). true \& will evaluate to the string true if the pattern match succeeds, and 0 otherwise, which, when paired with \| false, will replace all cases where it evaluated to 0 with the string false. Shaves off seven characters from my original approach (and six characters relative to the slightly shorter version I updated the question with, using =~ to avoid a pair of *s).

If anyone can come close to that with pure bash, or beat it using near-universally available tools like expr, let me know; I'll happily accept such an answer.

\$\endgroup\$
4
  • \$\begingroup\$ This seems to return true for any subsring of FIXEDSTRING. eg, set FIXED; expr true \& FIXEDSTRING : .*$1 \| false returns true. \$\endgroup\$
    – Jonah
    Feb 24, 2023 at 5:29
  • 1
    \$\begingroup\$ @Jonah: Yes, that's the goal. The original test is a substring test, the expr one is too. \$\endgroup\$ Feb 24, 2023 at 11:11
  • \$\begingroup\$ Ah. It's never actually stated in the question. Even though I could have figured it out if I'd read the python examples, the actual goal should probably be stated up front. \$\endgroup\$
    – Jonah
    Feb 24, 2023 at 14:56
  • 1
    \$\begingroup\$ @Jonah: I said it in the first paragraph, but I admittedly wasn't clear on which direction the substring checking was going (checking for $1 in FIXEDSTRING or FIXEDSTRING in $1). I've edited to make it more clear. \$\endgroup\$ Feb 24, 2023 at 15:16
2
\$\begingroup\$

Here's an option that abuses the filesystem for 41 bytes:

echo true>FIXEDSTRING
dd<*$1*||echo false

Attempt This Online!

This requires that your fixed string is not empty, ., .., nor contains /.

\$\endgroup\$
0
0
\$\begingroup\$

Found a sed-based solution that beats expr significantly. Obviously not a bash built-in, but it's using solely portable sed features AFAICT so it should be near universally available:

sed "/$1/ctrue
cfalse"<<<FIXEDSTRING

/$1/ctrue selects "lines" (there's only one line) which match the regex from $1, and ctrue short-circuits for that line, emitting true and discarding the line. If it wasn't short-circuited, cfalse with no selector handles everything else by echoing false and discarding the line.

Shaves four characters off the expr solution: Ignoring length of the fixed string, which appears once in each, it's 25 characters, vs. 29 for expr. It also has similar flexibility (it's not just true or false, you can use mostly arbitrary string outputs).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.