6

I have a changelog file in markdown which contains all changes between each version of my app like that :

## Version 1.0.6

* first change
* second change
* third change

## Version 1.0.5

* first foo change
* second foo change

## Version 1.0.4

* and so on...

What I want is to extract in a script the changes content for a version. For example I would to extract the changes for the version 1.0.5, so it should print :

* first foo change
* second foo change

The ideal way would be ./getVersionChanges version filename which would those 2 params :

version : the version to extract changes

filename : the filename to parse

How can I achieve this with sed, awk, grep, or whatever ?

ejay
  • 1,094
  • 3
  • 12
  • 26
  • Possible duplicate of [How to select lines between two patterns?](http://stackoverflow.com/questions/38972736/how-to-select-lines-between-two-patterns) – Sundeep Nov 06 '16 at 14:15
  • see also https://stackoverflow.com/documentation/awk/1403/variables#t=201611061418048683413 and https://stackoverflow.com/questions/2227583/passing-variable-to-awk-and-using-that-in-a-regular-expression – Sundeep Nov 06 '16 at 14:20

4 Answers4

8

A slightly more elaborate awk solution, which

  • exits once the block of interest has been printed,
  • ignores blank lines,
  • doesn't include the header line.
awk -v ver=1.0.5 '
 /^## Version / { if (p) { exit }; if ($3 == ver) { p=1; next } } p && NF
' file

As script getVersionChanges:

#!/usr/bin/env bash

awk -v ver="$1" '
 /^## Version / { if (p) { exit }; if ($3 == ver) { p=1; next } } p && NF
' "$2"

Explanation:

  • Regex condition /^## Version / matches the header line of a block of lines with version-specific information, by looking for substring ## Version at the start (^) of the line and, if found, executing the associated code block ({ ... }):

    • if (p) { exit } exits (stops processing), if the p (print) flag has previously been set, because that implies that the block after the one of interest has been reached, i.e. that the block of interest has now been fully processed.

    • if ($3 == ver) { p=1; next } checks if the 3rd whitespace-separated field ($3) on the header line matches the given version number (passed via option -v ver=1.0.5 and therefore stored in variable ver) and, if so, sets custom variable p, which serves as a flag indicating whether to print a line, to 1 and moves on to the next line (next), so as not to print the header line itself.
      In other words: p containing 1 indicates for subsequent lines that the version-specific block of interest has been entered, and that its lines should (potentially) be printed.

  • Condition p && NF implicitly prints the line at hand if the condition matches, which is the case if the print flag p is set and (&&) the line at hand has at least one field (based on the number of fields being reflected in built-in variable NF), i.e. if the line is non-blank, thereby effectively skipping empty and all-whitespace lines in the block of interest.

    • Note that both operands of && use implicit Boolean logic: a value of 0 (which a non-initialized custom variable such as p defaults to) is implicitly false, whereas any nonzero value implicitly true.
mklement0
  • 312,089
  • 56
  • 508
  • 622
3

Try this. You can replace /tmp/data with your file name and 'Version 1.0.5' with your search pattern. Note that this does not strip any empty lines.

sed  '1,/Version 1.0.5/d;/Version/Q' /tmp/data

Output:

* first foo change
* second foo change  

Explanation

By default sed will print the lines. So we just change the logic to delete the lines which we do not need.

Select everything between line 1 and pattern and delete it

 1,/Version 1.0.5/d

Quit when you find the pattern

 /Version/Q
Jay Rajput
  • 1,773
  • 14
  • 21
  • Intriguing, but to avoid false positives you should (a) escape the `.` in the version number as `\.` (which means that with input from a variable you'd have to perform this substitution _programmatically_ first) and (b) make sure that the version number only matches on a word boundary at the end too. Also, to make sure that a potential match is also found on the _first_ line, the range should start with `0` (`0,/Version 1\.0\.5\b/`). Starting the range with `0` and using the `Q` function are _GNU_ `sed` extensions, so on BSD/macOS a more elaborate solution is required. – mklement0 Nov 06 '16 at 17:23
  • 1
    I update the answer to indicate that it will not strip the empty lines. Other things which you pointed out are also correct. – Jay Rajput Nov 06 '16 at 17:30
3

A rather short awk script will extract the block you want.

#!/bin/sh

awk -v version="$1" '/## Version / {printit = $3 == version}; printit;' "$2"

A sample:

$ ./getVersionChanges 1.0.5 filename
## Version 1.0.5

* first foo change
* second foo change

$
chepner
  • 446,329
  • 63
  • 468
  • 610
  • should not `$3 == v` be `$3 == version` – Jay Rajput Nov 06 '16 at 14:20
  • the next command can be used inside the `{}` if the intent is to skip the line with the version. This is what the question has asked. – Jay Rajput Nov 06 '16 at 14:23
  • I know I'm contradicting the request, but I think it is helpful to include the header in the output (and it makes the code simpler). – chepner Nov 06 '16 at 14:25
  • Works well too, the difference with the accepted solution is that yours keeps the header of the version, thank you. – ejay Nov 06 '16 at 14:39
1

Keep a Changelog

I found out this thread while looking for a way to extract some release notes from a changelog written with the keepachangelog convention.

I have adapted @mklement0's answer to make it work with this convention.

Shell

awk -v ver=1.0.5 '
 /^#+ \[/ { if (p) { exit }; if ($2 == "["ver"]") { p=1; next} } p && NF
' file

Script

#!/usr/bin/env bash

awk -v ver="$1" '
 /^#+ \[/ { if (p) { exit }; if ($2 == "["ver"]") { p=1; next} } p && NF
' "$2"
vdsbenoit
  • 191
  • 2
  • 7