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,114
  • 3
  • 14
  • 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

5 Answers5

12

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
  • 382,024
  • 64
  • 607
  • 775
4

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
  • 497,756
  • 71
  • 530
  • 681
  • 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
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,813
  • 17
  • 23
  • 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

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
  • 321
  • 3
  • 7
1

Building off of @mklement0's great answer, here's a rather scuffed version (forgive my lack of AWK script knowledge) that works with Conventional Changelog with the assumptions:

  • ## or ### starts the version line
  • Followed up immediately by a space and a semantic version number that may or may not be surrounded with brackets (e.g. either ## 1.23.456 or ## [1.23.456](http://github.com/etc))

Unlike the OP, I...

  • Want to keep the version line (if you don't want to keep the version line, add next after p = 1 like p = 1; next).
  • Want to retain linebreaks (if you want to remove linebreaks, add NF after p like p && NF)

CHANGELOG.md

# Change Log

All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

## [1.23.456](https://github.com/your-org/your-repo/branches/compare/foo@1.23.456%foo@1.23.455) (2022-10-19)

**Note:** Version bump only for package foo

## 1.23.455 (2022-10-19)

**Note:** Version bump only for package foo

### Bug fixes

* **css:** Some style fix
* **js:** Some js fix
* **html:** Some html fix

### Features
* **js:** Some js feature

AWK

awk -v ver=5.1.30 '
 /^(##|###) \[?[0-9]+.[0-9]+.[0-9]+/ {
    if (p) { exit };
    if (index($2, "[")) {
        split($2, a, "[");
        split(a[2], a, "]");
        if (a[1] == ver) {
            p = 1
        }
    } else {
        if ($2 == ver) {
            p = 1
        }
    }
} p
' path/to/CHANGELOG.md

awk -v ver=1.23.456 ...

## [1.23.456](https://github.com/your-org/your-repo/branches/compare/foo@1.23.456%foo@1.23.455) (2022-10-19)

**Note:** Version bump only for package foo

awk -v ver=1.23.455 ...

## 1.23.455 (2022-10-19)

**Note:** Version bump only for package foo

### Bug fixes

* **css:** Some style fix
* **js:** Some js fix
* **html:** Some html fix

### Features
* **js:** Some js feature
Kerry Johnson
  • 842
  • 9
  • 16