Inspired by Tool for creating CodeReview questions.

This script generates a markdown document suitable for a CR question from a list of files. For each file it finds, it sends the following to stdout:

  • The relative path to the file, in bold.
  • The byte count and line count, in parentheses.
  • The contents of the file, indented four spaces on each line.

./mdproj.sh (186 bytes in 7 lines)

for path in $(find "$@"); do
    bytes="$(wc -c < "$path") bytes"
    lines="$(wc -l < "$path") lines"
    echo -e "\n**$path** ($bytes in $lines)\n"
    sed "s/^/    /" < "$path"

The script takes the same options as find. Example usage:

mdproj.sh -name *.h -or -name *.cpp > whatever.md

Update / known issues

  • No spaces, backquotes, backslashes, asterisks, or other weird characters are allowed in file names.

  • Watch out for shell expansion when invoking the script. Arguments should be enclosed in quotes in cases where the the shell would expand them.

  • 1
    \$\begingroup\$ Now who will make a .bat version of this? :) \$\endgroup\$ Commented Jun 9, 2014 at 0:01
  • \$\begingroup\$ I'm not sure if it would be possible with the tools that ship with Windows; probably needs some things from GnuWin32 or similar \$\endgroup\$
    – Dagg
    Commented Jun 9, 2014 at 0:10
  • \$\begingroup\$ Use cygwin FTW. \$\endgroup\$
    – rolfl
    Commented Jun 9, 2014 at 0:20

1 Answer 1


I note three potential problems with your approach, both dealing with special characters in filenames, as well as one comment on the example usage.

for with find

Consider a directory structure where some paths have spaces in them, say:

├── Constants.h
└── Main Loop.c

(One could argue the wisdom of using spaces in paths, but your script should be able to deal with them anyway.) What would find . -type f output?

./Source/Main Loop.c

What would for path in $(find . -type f); do echo $path; done output?


That's right; for splits on whitespace, breaking your paths. This is a notorious issue. Fortunately, that page provides a solution:

find . -type f -print0 | while IFS= read -r -d '' path; do echo $path; done

echo -e

Look at this line:

echo -e "\n**$path** ($bytes in $lines)\n"

You're substituting path in, and then telling echo to interpret the resulting text. That suggests that if path has an escape sequence in it, echo will interpret it. Indeed, it does:

% path='x\ay'
% touch "$path"
% echo -e "$path"

(The \a was interpreted as "sound the bell.") Instead of using echo -e, consider passing the special characters directly to echo. You can make Bash interpret ones before passing it to echo by using $'':

% echo $'\n'"**$path** ($bytes in $lines)"$'\n'


Markdown cares about special names, too. What if I had a file named a** b **c? Your script would give me a header:

**a** b **c**

a b c

But that's probably not what you intended. You probably want to enclose it in backticks:

**`a** b **c`**

a** b **c

But even that's not enough. What if my file names have backticks, like, say, a` b `c?

**`a` b `c`**

a b c

That's no good! You have to then double-backtick it:

**``a` b `c``**

a` b `c

Essentially, you have to count the maximum number of consecutive backticks and enclose it in that-number-plus-one of backticks.

Example usage

You suggest

mdproj.sh -name *.h -or -name *.cpp > whatever.md

but that will only work when there are exactly zero or one files whose name end with .h and exactly zero or one files whose name end with .cpp. Consider how it expands when there are no files:

mdproj.sh -name *.h -or -name *.cpp > whatever.md

That's right; Bash leaves them in place, passing them on to find, where it looks for files of the form *.h and *.cpp. Since there are no such files (otherwise, Bash would have substituted them in) find will not find the files, and so nothing will happen. What if there is example.h and example.cpp?

mdproj.sh -name example.h -or -name example.cpp > whatever.md

That works fine. What if there are multiple?

mdproj.sh -name foo.h bar.h -or -name foo.cpp bar.cpp > whatever.md

Uh oh. -name takes only one value, but you're passing it two. My find dies with this error:

find: bar.h: unknown option

I always use -regex for that sort of thing:

mdproj.sh -regex '.*\.h' -or -regex '.*\.cpp' > whatever.md

(If I was being cleverer, I might use -E and merge them into one regular expression.)

  • \$\begingroup\$ Thanks for the comments! re: "that will only work when there are exactly zero or one files whose name end with .h and exactly zero or one files whose name end with .cpp." -- I might be reading this wrong, but it does work with multiple files. Am I misunderstanding you? When I run this the shell does not expand *.foo, it gets passed through as a pattern to find. \$\endgroup\$
    – Dagg
    Commented Jun 9, 2014 at 1:45
  • \$\begingroup\$ @Dagg: The shell expands it and breaks the command for me. With a.h, b.h, a.cpp, and b.cpp in the current working directory, I get find: b.h: unknown option. It does, however, work if they are in a subdirectory rather than the current working directory. \$\endgroup\$
    – icktoofay
    Commented Jun 9, 2014 at 1:47
  • \$\begingroup\$ Ah, yes, all of my code was in a subdirectory, so of course it wasn't expanding it. Silly mistake :) \$\endgroup\$
    – Dagg
    Commented Jun 9, 2014 at 1:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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