0

I have semantic versioning for an app component and this is how I update the major version number and then store it back in version.txt. This seems like a lot of lines for a simple operation. Could someone please help me trim this down? No use of bc as the docker python image I'm on doesn't seem to have that command.

This is extracted from a yml file and version.txt only contains a major and minor number. 1.3 for example. The code below updates only the major number (1) and resets the minor number to 0. So if I ran the code on 1.3, I would get 2.

- echo $(<version.txt) 1 | awk '{print $1 + $2}' > version.txt
  VERSION=$(<version.txt)
  VERSION=${VERSION%.*}
  echo $VERSION > version.txt
  echo "New version = $(<version.txt)"
Ludo
  • 2,307
  • 2
  • 27
  • 58
  • 3
    It's not *that* many lines, frankly. Most of what worries me about it isn't the number of lines, but the places where the code is ambiguous -- `echo $VERSION`, for example, will treat the unquoted value as subject to string-splitting and globbing. – Charles Duffy May 15 '18 at 14:42
  • Generally speaking, seeking a one-liner at the expense of readability, robustness, &c. is prioritizing the wrong things. – Charles Duffy May 15 '18 at 14:42
  • Show us the how the file looks like - `version.txt` – Inian May 15 '18 at 14:42
  • That said, what's with the leading dashes? If this is extracted from a YAML configuration file of some kind, we need to know that. – Charles Duffy May 15 '18 at 14:43
  • And most likely no need for anything but a single `awk` script. – Kusalananda May 15 '18 at 14:43
  • Is it changing `12.43` into `13`? – Kusalananda May 15 '18 at 14:44
  • 1
    Presumably we need to deal with `12.43.56`, not just `12.43`, if it's really semver. – Charles Duffy May 15 '18 at 14:48
  • BTW, since you're on a slim Docker image, I'm a little surprised you have `$( – Charles Duffy May 15 '18 at 14:52
  • ...the goal of simplicity should be correctness. When you're achieving terseness by using ambiguous primitives (that is, components that can do undesired things), that terseness is not helping either simplicity *or* correctness: Your real behavior is not simple (since it includes undesired and unexpected corner cases), or correct. – Charles Duffy May 15 '18 at 15:03
  • I am just using a major and minor number to simplify things. And no @Kusalananda, it would change 12.43 to 13 – Ludo May 15 '18 at 15:10
  • Your code looks like it's structured as part of a YAML list. What are you running it through? If it's run as five separate shell invocations, then variables aren't persisted across those commands. – Charles Duffy May 15 '18 at 15:10
  • I am running it through bitbucket-pipelines and the variables seem to persist. – Ludo May 15 '18 at 15:11
  • 1
    I would suggest putting a multi-line script as a single list entry, just to be unambiguous (ie. to folks coming from ansible or other tooling where each shell command in a list is run by a separate shell). One dash and space at the front, and then just indent all later lines to start at the same indent level. – Charles Duffy May 15 '18 at 15:15

1 Answers1

3

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.
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • They seem to remove everything after the last dot. – Kusalananda May 15 '18 at 14:46
  • If they want to go from `13.2.4` to `14`, then it's just `echo "$((major + 1))" >version.txt`, so even easier. A detailed specification would make it easier to be confident in an answer. – Charles Duffy May 15 '18 at 14:46
  • Thanks for your detailed answer! When I run this in bitbucket pipelines: `- IFS=. read -r major rest "version.txt" && mv "version.txt"` I get: `mv: missing destination file operand after ‘version.txt’ Try 'mv --help' for more information.` I removed the if else statement as it would have to all go onto one line in a bitbucket pipelines yml file and would have been unreadable – Ludo May 15 '18 at 15:54
  • YAML absolutely does allow multi-line literals. If bitbucket uses a real compliant YAML parser, you absolutely can split your literals across multiple lines. – Charles Duffy May 15 '18 at 16:51
  • ...that said, some of my code was just broken -- please see updates. – Charles Duffy May 15 '18 at 16:52