70
\$\begingroup\$

What general tips do you have for golfing in Bash? I'm looking for ideas that can be applied to code golf problems in general that are at least somewhat specific to Bash (e.g. "remove comments" is not an answer). Please post one tip per answer.

\$\endgroup\$

43 Answers 43

1
2
2
\$\begingroup\$

Print the first word in a string

If the string is in the variable a and doesn't contain escape and format characters (\ and %), use this:

printf $a

But it would be longer than the following code if it is needed to save the result into a variable instead of printing:

x=($a)
$x
\$\endgroup\$
2
\$\begingroup\$

It is almost always better to receive input as arguments to a script or function instead of on stdin. Examples:

Input: a string:

# == stdin ==
read a;echo $a
echo `<&0`
# == arguments ==
echo "$1"       # if $1 can have globbing characters/newlines/tabs
echo $1         # if $1 does not have any problematic characters

Input: a list of strings:

# == stdin ==
while read s; do echo $s; done          # one-time use, split on newlines
read -a a;for s in ${a[@]};{echo $s;}   # read one line from stdin; splits on spaces
mapfile a;for s in "${a[@]}";{echo $s;} # read to EOF, split on newlines
# == arguments ==
for s;{ echo $s;}

The exception: a list of strings and other arguments:

read i;while read s; do echo ${s[$i]}; done   # both on stdin
i=$1;shift;for s;{ echo ${s[$i]};}            # both as arguments
read i;for s;{ echo ${s[$i]};}                # list as arguments, other as stdin

Additionally, read will mangle backslashes without -r, and will stop on newlines without -d '', making it even more expensive in worst case scenarios.

\$\endgroup\$
1
  • \$\begingroup\$ Read lines l from a file p, in Zsh: for l ("${(f)$(<p)}") \$\endgroup\$
    – roblogic
    Commented Dec 27, 2022 at 6:04
2
\$\begingroup\$

In Pure Bash (or any other pure shellscript), use . for looping.

For example, if you want to display each value of 1^2, 2^2, 3^2, ..., 999^2, 1000^2,

echo $[++x*x]
((x>999))||. x

(where filename is x) is shorter than

for((;x++<1000;));{
echo $[x*x]
}

Try it online!

\$\endgroup\$
1
  • \$\begingroup\$ I noticed this can be applied to non-pure shell script too. \$\endgroup\$
    – user100411
    Commented Nov 13, 2021 at 7:04
2
\$\begingroup\$

Know grep options

See also man 1p grep and manual for GNU Grep.

grep -x foo is equivalent to grep '^foo$

This is POSIX-compatible.

Consider only input lines that use all characters in the line excluding the terminating <newline> to match an entire fixed string or regular expression to be matching lines.

For example, the following two are equal:

grep -E '^abc$|^def$'
grep -F -x 'abc
def'
\$\endgroup\$
1
\$\begingroup\$

Doing 2 embed loop with 1 for instruction:

for ((l=i=0;l<=99;i=i>98?l++,0:++i)) ;do
    printf "I: %2d, L: %2d\n" $i $l
done |
    tee >(wc) | (head -n4;echo ...;tail -n 5)
I:  0, L:  0
I:  1, L:  0
I:  2, L:  0
I:  3, L:  0
...
I: 96, L: 99
I: 97, L: 99
I: 98, L: 99
I: 99, L: 99
  10000   40000  130000
\$\endgroup\$
1
\$\begingroup\$

Assign and Print quoted strings

If you want to assign a quoted string to a variable, and then print the value of that variable, then the usual way to do that would be:

a="Programming Puzzles & Code Golf";echo $a

If a was previously unset, this may be shortened to:

echo ${a=Programming Puzzles & Code Golf}

If a was previously set, then this should be used instead:

echo ${a+Programming Puzzles & Code Golf}

Note this is only useful if the string requires quotes (e.g. contains whitespace). Without quotes, a=123;echo $a is just as short.

\$\endgroup\$
1
  • 2
    \$\begingroup\$ ${a+foo} doesn't set a. \$\endgroup\$ Commented Oct 20, 2019 at 6:28
1
\$\begingroup\$

In a case statement, it is valid to omit the last ';;' - which saves 2 bytes:

#!/bin/sh

case $I in
0)cmdA;;
1)cmdB;;
*)cmdC
esac
\$\endgroup\$
1
\$\begingroup\$

Use {..} expansions with eval

Occasionally loops can be shortened by using eval with a brace expansion. For example, to run a command foo with all letters a-z sequentially; instead of:

for x in {a..z};{ foo $x;}
# or, zsh only:
for x ({a..z})foo $x
eval foo\ {a..z}\;
# or, zsh only:
eval ';foo '{a..z}

If you need to reuse the loop variable, it tends to be shorter to use a normal for-loop though.

I can't remember exactly the first answer I saw to do this, but I now use it a lot (those examples are all Zsh because it's obviously superior to Bash, but it's a similar general idea).

\$\endgroup\$
1
\$\begingroup\$

Compress long runs of decimal digits using alternate base representation

bash's arithmetic context supports numbers in arbitrary alternate bases more compactly that any other language I know of (e.g. Python's int(string,base) has overhead of five characters for the int, (, ), and ,, if you aren't given the string from elsewhere, two more for the ' or " around the literal, plus 1-2 characters for the base). By contrast, bash only needs three characters to impose arithmetic context, $[] (if you're not already in it), and just one character to impose alternate base context, and the same 1-2 characters for the base, so bash needs 2-6 characters to do what Python (which is relatively good on this score) needs 6-9 characters to do.

Bash also supports higher bases for integer literals than most languages (topping out at base 64, rather than base 36 as is common; if you use a base above 36, it uses a digit "alphabet" of up to 0-9a-zA-Z@_).

Between these two features, you can shave up to three characters off long strings of decimal digits by replacing them with arithmetic context (using the shorter form of arithmetic expansion) of a base 64 value. The best savings occur when the digits in question require an even multiple of six bits to represent in binary, and no savings are available unless the decimal representation of the integer is at least 15 characters long, but when it works, it's a great way to shorten further. If you're using it as a number in an existing arithmetic context that does other math, you can save up to 6 characters (since the $[] is already paid for by the need to do math, so it doesn't count against the cost of the alternate base), and benefits begin to appear at just 8 decimal digits.

For the best case scenario, a program that needs a literal string numerically equivalent to a number between (with _ for readability) 1_000_000_000_000_000_000 and 1_152_921_504_606_846_975 can represent such a number as either:

1000000000000000000
# or shaving three characters
$[64#TwJHeDp000]

If you were already in arithmetic context, it saves six instead:

$[var%1000000000000000000]
# becomes
$[var%64#TwJHeDp000]
\$\endgroup\$
0
\$\begingroup\$

Changing behaviour of += by setting integer flag

Try this many times:

unset i
i=1
for a in {1..10} ;do
    i+=1
    ((RANDOM%9)) || declare -i i
  done
echo $i
11117

or in one line

unset i;i=1;for a in {1..10};do i+=1;((RANDOM%9))||declare -i i;done;echo $i

This could answer something between 20 to 11111111111...

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

This work under simple shell

cd `mktemp -d` &&>'c'a't'
ln -s /proc/loadavg /proc/uptime .

Then now

procs=$(*)
echo $procs
0.30 0.08 0.03 1/612 31671 322787.60 1259967.99

Nota: Of course, >cat could be written >$(echo -e \\0143\\0141t)

Care, from there, you could encouter some issues due to Locales!

Goto last demo using LC_ALL=C

Same way:

mv cat grep
mv loadavg Mem
ln -s /proc/meminfo Zdatas
rm uptime

mems=$(*)
echo $mems 
MemTotal: 16386788 kB MemFree: 9816320 kB MemAvailable: 12144892 kB

or worst...

mv grep sed
mv Mem s+\\\(Mem\\\|Swap\\\).\*\:++p\;d

then

memsw=$(*)
echo $memsw 
16386788 kB 9834080 kB 12163596 kB 0 kB 20971516 kB 20971516 kB

So simple is this!

declare -p procs mems memsw
declare -- procs="0.14 0.12 0.05 1/612 32078
324221.56 1265616.26"
declare -- mems="MemTotal:       16386788 kB
MemFree:         9832212 kB
MemAvailable:   12161688 kB"
declare -- memsw="       16386788 kB
         9834080 kB
   12163596 kB
            0 kB
      20971516 kB
       20971516 kB"

Last test using LANG=C

(or not)

cd `mktemp -d` &&>'c'a't'
ln -s /proc/meminfo zdatas
cp c* y+-+-+\;s+\\\(Mem\\\|Swap\\\).\*\:\ \*++p\;d
rename 'y/act/esd/' ???
*

may render something like

378908 kB
31940 kB
217924 kB
0 kB
102396 kB
102396 kB

Or

cd `mktemp -d`
ln -s +++\;s+\\\(Mem\\\|Swap\\\).\*\:\ \*++p\;d /proc/me*o ed .
rename 's/^/chr(113+(20>length?1.3*length:8))/e' *
*
\$\endgroup\$
1
0
\$\begingroup\$

To print a range of characters (shorter than echo {a..z}|tr -d ' '):

printf "%s" {a..z}

Print range in reverse order (shorter than printf "%s" {a..z}|rev):

printf "%s" {z..a}
\$\endgroup\$
1
  • 2
    \$\begingroup\$ Why not echo {a..z} ? \$\endgroup\$
    – Nate T
    Commented Nov 20, 2021 at 5:12
-1
\$\begingroup\$

ONE-LINE CONDITIONAL SETTER

NOTE: I see now that this first bit is already present on the first page, so I updated to add another tip (below the phrase 'EDIT-POINT:') which adds to the initial syntax to achieve a different outcome.

Here is a super efficient way to do control statements in bash.

instead of

if [ -e "${var}" ]
then
   echo "${var}"
else
   echo "empty"
fi

which translates to the one-liner:

if [ -e "${var}" ]; then echo "${var}"; else echo "empty"; fi

...you can use logical and / or like so:

[ -e "${var}" ] && echo "${var}" || echo "empty"

The second bit runs only if the first bit returns a 0 exit code. If either of the first two bits fail to exit with 0 (in other words, if false when used with [] / test command), the third bit runs. This means that in production / distribution use cases, this form is often a bad idea (if the if logic runs but fails fails, the else statement runs..), but for code golf, it is ore than sufficient, so long as you test it on the command line first.

It also works with the if / && (no else / ||), e.g.

[ -e "${var}" ] && echo "${var}"

if that is what you need. This version runs exactly as the long form, without the caveat mentioned above.

EDIT-POINT:

Another advantage of using this syntax is that you can put the entire command inside a variable. When you need to set a variable to a value that is based on a condition, this saves a lot of space.

For example,

if [ -e "${var}" ]
then
   TEXT="${var}"
else
    TEXT="empty"
fi

can be dropped to:

TEXT=$([ -e "${var}" ] && echo "${var}" || echo "empty")

As a code golf tip, this has limited uses, but they do exist. The space gained or lost depends on the size of the variable name. It adds up to the same as setting a four-letter variable (so ends in unity in the example above.)

Since we use single-letter variable names in code golf, this normally doesn't save any space at all. The space saving effect kicks in when we have no control over the name, in cases where we are using an environment variable in a cheeky way, or when a variable name is provided via a constraint in the question.

\$\endgroup\$
4
  • \$\begingroup\$ -1 because duplicated. \$\endgroup\$
    – user100411
    Commented Nov 20, 2021 at 9:00
  • 1
    \$\begingroup\$ @nrgmsbki4spot1 Thank you for commenting. I didnt even notice there were 2 pages. Deleting, but I will wait a few hours so you have time to see comment / thank you. I know thank yous are looked down upon in some of the sites, but for me, they are my favorite part, and honestly, they are what keeps me coming back. On that note, thanks again. \$\endgroup\$
    – Nate T
    Commented Nov 21, 2021 at 0:43
  • \$\begingroup\$ @nrgmsbki4spot1 Decided to edit instead. Added Another 'trick' which builds on the original. I left the original bit, as the new one wouldn't make much sense without it. Is this better? \$\endgroup\$
    – Nate T
    Commented Nov 21, 2021 at 6:06
  • 1
    \$\begingroup\$ As the first tip was previously included, I would assume that I am okay in the way of "one tip per answer", and I do feel it adds value. Sorry to bother you again. I am new to this SE, so I am still learning the sites nuances. Btw, if your wondering how someone could miss the first page altogether, its because I found it via link. \$\endgroup\$
    – Nate T
    Commented Nov 21, 2021 at 6:14
1
2

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.