Short version

I would like to create a tabular display of multiline text, similar to the following:

all       Build all targets
document  Create documentation of source files in the subfolders
          `src` and `script`, and write it to `man`
test      Run unit tests

At the moment, my input for this looks as follows, but this can of course be changed:

all---Build all targets
document---Create documentation of source files in the subfolders `src` and `script`, and write it to `man`
test---Run unit tests

I’ve tried achieving this with a combination of awk and wrap/pr but while the line wrapping works, the indentation doesn’t. Here’s my current approach:

…
| awk -F '---' "{ printf '%-10s %s\n', $1, $2 }" \
| fold -w $(($COLUMNS - 1)) -s

It generates the output

all       Build all targets
document  Create documentation of source files in the subfolders
`src` and `script`, and write it to `man`
test      Run unit tests

… in other words, the third line isn’t indented as intended.

How can I format the text with a given wrap length and a given hanging indent width? — Without changing anything else about the text. Bonus: this should work with UTF-8 and escape/control characters.


Background info

The goal is to create self-documenting Makefiles. As a consequence, the logic to format and display the code should be small, self-contained, and not rely on separately installed software; ideally, it should work on any system that can execute Makefiles, hence my restriction to (something close to) coreutils.

That said, I briefly tried solving the problem using groff but this became too complex very quickly (and OS X groff is and old version that doesn’t seem to support UTF-8).

The original string that I’m trying to parse and format therefore looks rather as follows:

## Build all targets
all: test document

## Run unit tests
test:
    ./run-tests .

## create documentation of source files in the subfolders `src` and `script`,
## and write it to `man`
document:
    ${MAKE} -C src document
    ${MAKE} -C script document

At the moment, this is parsed using a sed script (see link for details) that ignores multiline comments, before being fed to the formatting code posted above.

  • You should be able to write the whole script in awk (sed and fold do not really add to the solution). – Thomas Dickey Apr 30 '16 at 13:04
  • @ThomasDickey Can awk easily wrap lines at whitespace? the regular expression and text manipulation tools of awk are rather primitive … short of manually implementing the word-wrapping inside a (nested) loop. – Konrad Rudolph Apr 30 '16 at 13:27
  • awk can compute a length, determine the place to split a line and format a continuation line. Those are easier to do than sed and wrap (and if I were writing this sort of application, would make it more portable than you propose). – Thomas Dickey Apr 30 '16 at 13:32
  • @ThomasDickey Replacing sed by awk is of course possible but (a) I need to have two separate commands because I sort the entries in-between parsing and formatting, and POSIX awk doesn’t have built-in sort (gawk does); and (b) simply extracting the documentation seems simpler in sed than awk. – Konrad Rudolph May 3 '16 at 11:58
  • sort is not mentioned in the question.. – Thomas Dickey May 3 '16 at 12:17
up vote 4 down vote accepted

With gnu awk you can do something simple like this:

awk -F '---' '
{ gsub(/.{50,60} /,"&\n           ",$2)
  printf "%-10s %s\n", $1, $2 }'

For a more accurate long-winded version handling long words:

awk -F '---' '
{ printf "%-10s ", $1
  n = split($2,x," ")
  len = 11
  for(i=1;i<=n;i++){
   if(len+length(x[i])>=80){printf "\n           "; len = 11}
   printf "%s ",x[i]
   len += 1+length(x[i])
  }
  printf "\n"
}'
  • This is almost perfect. The only problem is that the lower bound — 50 — is arbitrary and will break if the input contains a word with more than 10 characters at the crucial position. Any suggestion for how to fix this? Using a lower number won’t work because then the match seems to happen non-greedily (actually I’m not sure what’s happening because it’s not simply matching single words). – Konrad Rudolph Apr 30 '16 at 14:46
  • Yes long words are a problem for this simple code. I added some more laborious handling for better results. – meuh Apr 30 '16 at 15:57
  • Ah hm. I was precisely trying to avoid that and use a pre-baked solution instead to handle edge cases properly: for instance, the above doesn’t handle dashes nicely — but, it turns out, neither does fold. So I’ll go with this. – Konrad Rudolph May 3 '16 at 11:22
  • I believe that this is not specific to GNU AWK, testing it on a fresh FreeBSD installation with no GNU utilities, I get the desired output. – forquare May 3 '16 at 12:26

Here's a shorter answer that uses fold then shifts its output by 11 spaces. To see what it is doing add a -v or -x to the final bash.

| sed 's:\(.*\)---\(.*\):printf "%-10s " "\1";fold -w '$(($COLUMNS - 11))' -s <<\\!|sed "1!s/^/           /"\n\2\n!\n:' | bash 

After the fold command pipe the output to sed and replace the start of line with a tab. And you can control the indent with with the 'tabs ' command prior:

tabs 5
echo "A very long line that I want to fold on the word boundary and indent as well" | fold -s -w 20  | sed -e "s|^|\t|g"
     A very long line
     that I want to fold
     on the word
     boundary and indent
     as well

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.