About Simplicity
"Simple" and "short" are not the same thing. echo $foo is shorter than echo "$foo", but it actually does far more things: It splits the value of foo apart on characters in IFS, evaluates each result of that split as a glob expression, and then recombines them.
Similarly, making your code simpler -- as in, limiting the number of steps in the process it goes through -- is not at all the same thing as making it shorter.
Incrementing One Piece, Leaving Others Unmodified
if IFS=. read -r major rest <version.txt || [ -n "$major" ]; then
echo "$((major + 1)).$rest" >"version.txt.$$" && mv "version.txt.$$" version.txt
else
echo "ERROR: Unable to read version number from version.txt" >&2
exit 1
fi
Incrementing Major Version, Discarding Others
if IFS=. read -r major rest <version.txt || [ -n "$major" ]; then
echo "$((major + 1))" >"version.txt.$$" && mv "version.txt.$$" "version.txt"
else
echo "ERROR: Unable to read version number from version.txt" >&2
exit 1
fi
Rationale
Both of the above are POSIX-compliant, and avoid relying on any capabilities not built into the shell.
IFS=. read -r first second third <input reads the first line of input, and splits it on .s into the shell variables first, second and third; notably, the third column in this example includes everything after the first two, so if you had a.b.c.d.e.f, you would get first=a; second=b; third=d.e.f -- hence the name rest to make this clear. See BashFAQ #1 for a detailed explanation.
$(( ... )) creates an arithmetic context in all POSIX-compliant shells. It's only useful for integer math, but since we split the pieces out with the read, we only need integer math. See http://wiki.bash-hackers.org/syntax/arith_expr
- Writing to
version.txt.$$ and renaming if that write is successful prevents version.txt from being left empty or corrupt if a failure takes place between the open and the write. (A version that was worried about symlink attacks would use mktemp, instead of relying on $$ to generate a unique tempfile name).
- Proceeding through to the write only if the
read succeeds or [ -n "$major" ] is true prevents the code from resetting the version to 1 (by adding 1 to an empty string, which evaluates in an arithmetic context as 0) if the read fails.
echo $VERSION, for example, will treat the unquoted value as subject to string-splitting and globbing.version.txt12.43.56, not just12.43, if it's really semver.