├── .dockerignore ├── screenshot.png ├── Dockerfile ├── Makefile ├── LICENSE ├── make-help.cr └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | **/* 2 | !make-help.cr -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xanders/make-help/HEAD/screenshot.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build 2 | 3 | FROM crystallang/crystal:1.0.0-alpine as builder 4 | 5 | COPY make-help.cr /make-help.cr 6 | 7 | RUN crystal build --release --static /make-help.cr 8 | 9 | # Run 10 | 11 | FROM scratch 12 | 13 | COPY --from=builder /make-help /make-help 14 | 15 | ENTRYPOINT ["/make-help"] -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Show this help message 2 | # Multiple lines are supported too 3 | # When it's first, both commands works: 4 | # `make` 5 | # `make help` 6 | help: 7 | @cat $(MAKEFILE_LIST) | docker run --rm -i xanders/make-help 8 | 9 | # Build Docker image 10 | build: 11 | docker build -t xanders/make-help . 12 | 13 | # Run `build` and `help` successively 14 | test: build help 15 | 16 | ## 17 | ## Section for authorized contributors 18 | ## 19 | 20 | # Push image to `https://hub.docker.com` 21 | push: 22 | docker push xanders/make-help -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Xanders 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /make-help.cr: -------------------------------------------------------------------------------- 1 | def make_help(input) 2 | sections = input.scan(/(?:##\s*\n## ([^\n]+?)\s*\n##\s*\n|\A)(.+?)(?=\z|\n##\s*\n## [^\n]+\n##\s*\n)/m) 3 | 4 | sections_with_goals = sections.map do |match| 5 | {match[1]?, match[2].scan(/((?:^# [^\n]+\n)+)^([^#:\n]+):(?!=)/m)} 6 | end 7 | 8 | commands = sections_with_goals.map(&.last).flatten.map(&.[2]) 9 | 10 | return "No commands with comments found" if commands.empty? 11 | 12 | indent = commands.map(&.size).max + 1 13 | 14 | sections_with_goals.map do |(section, goals)| 15 | result = goals.map do |match| 16 | comments, command = match[1].lines, match[2] 17 | 18 | help = comments[0].strip + comments[1..-1].map do |comment| 19 | "\n " + " " * indent + comment 20 | end.join 21 | 22 | # Highlight `code` 23 | help = help.gsub(/\`.+?\`/) do |code| 24 | "\033[36m#{code}\033[0m" 25 | end 26 | 27 | "\n \033[32m#{command.ljust indent}\033[0m#{help}" 28 | end 29 | 30 | result.unshift("\n \033[1m\033[31;1m#{section}\033[0m") if section 31 | 32 | next result 33 | end.flatten.compact.unshift("Commands:").join("\n") 34 | end 35 | 36 | puts "\nUsage: make \033[32m\033[0m [ARGUMENT1=value1] [ARGUMENT2=value2]\n\n" 37 | 38 | puts make_help(STDIN.gets_to_end) 39 | 40 | puts -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # make-help 2 | 3 | Is it crazy enough to show the `Makefile` help via [Docker](https://www.docker.com)? This is exactly what I did. 😋 4 | 5 | ## Why? 6 | 7 | I often use [`make`](https://www.gnu.org/software/make) as a uniform way to document tasks for cross-language projects. 8 | For example, `make test` in any project will run tests, `make build` will always rebuild Docker images. 9 | In any project, the first thing I do is go to the `Makefile` to remember how to work here. 10 | 11 | But it will be much easier not to read the entire `Makefile`, but to run `make help` or even just `make` and see all the information I need. 12 | 13 | So this project aims to provide a very simple way to achieve it. 14 | 15 | ## How? 16 | 17 | Just add this to the top of the `Makefile`: 18 | 19 | ```make 20 | # Show this help 21 | help: 22 | @cat $(MAKEFILE_LIST) | docker run --rm -i xanders/make-help 23 | ``` 24 | 25 | That's all. Now you can use both `make` and `make help` commands. No additional dependencies are required (with except for [Docker](https://www.docker.com), obviously). 26 | 27 | Add comments to all public tasks. You can use multi-line comments as well as code examples in `` `backticks` ``. 28 | No special syntax required, just use single `#` as usual. 29 | 30 | Thanks to [@muuvmuuv](https://github.com/muuvmuuv) ([#1](https://github.com/Xanders/make-help/issues/1)), you can also add sections to a large `Makefile` like this: 31 | 32 | ```make 33 | ## 34 | ## Section name (one line) 35 | ## 36 | ``` 37 | 38 | **Please see the example in [this project `Makefile`](Makefile).** 39 | 40 | ![Screenshot with this project help output](screenshot.png) 41 | 42 | *Screenshot from Windows Terminal, so this is a relatively platform-independent solution.* 43 | 44 | ## What?? 45 | 46 | `@` before `cat` means "Do not show this line when I run the task". 47 | 48 | `MAKEFILE_LIST` is a variable with current `Makefile` path. `$(...)` used to substitute variable name with it's value. 49 | 50 | `cat` with pipe (`|`) puts the file contents into the next command. 51 | 52 | `--rm` means "Delete Docker container immediately after finishing". 53 | 54 | `-i` means "Allow to get information from Docker's standard input". 55 | 56 | `xanders/make-help` is a name of [the image at Docker Hub](https://hub.docker.com/r/xanders/make-help). 57 | 58 | So we grab current `Makefile` and push to the container with a program. 59 | It processes the file and outputs usage help with the tasks comments. 60 | 61 | The image [is built from scratch](https://docs.docker.com/develop/develop-images/baseimages/#create-a-simple-parent-image-using-scratch) 62 | and is as small as even possible in Docker: 932.34 **KB** compressed (two layers). 63 | 64 | I used [extra-fast](https://github.com/kostya/crystal-benchmarks-game) [Crystal programming language](https://crystal-lang.org) with [static linking](https://crystal-lang.org/reference/using_the_compiler/index.html#creating-a-statically-linked-executable) on [Alpine-based compiler](https://crystal-lang.org/2020/02/02/alpine-based-docker-images.html) at [build stage](https://docs.docker.com/develop/develop-images/multistage-build). 65 | 66 | So it will be fast enough and also tiny on disk. 67 | 68 | ## Alternatives 69 | 70 | There is a lot of similar projects in different languages, for example: 71 | 72 | * A **lot** of [Bash solutions](https://gist.github.com/prwhite/8168133) 73 | * [Go](https://github.com/Songmu/make2help) 74 | * [JavaScript](https://github.com/ianstormtaylor/makefile-help) 75 | * [Python](https://github.com/mrdor44/MakeHelp) 76 | * [Perl](https://github.com/christianhujer/makehelp) 77 | 78 | You even can use this one-line Ruby version: 79 | 80 | ```make 81 | # Show this help 82 | help: 83 | @cat $(MAKEFILE_LIST) | ruby -e "input = ARGF.read; indent = input.scan(/^\w[^:]*:/).map(&:size).max + 1; puts \"\nUsage: make \033[32m\033[0m [ARGUMENT1=value1] [ARGUMENT2=value2]\n\nCommands:\n\", input.scan(/((?:^# .+\n)+)^([^:]+):/).map { |comments, command| \"\n \033[32m#{command.ljust indent}\033[0m#{comments.lines.first.strip}#{comments.lines[1..-1].map { |comment| \"\n \" + ' ' * indent + comment }.join}\".gsub(/\`.+?\`/) { |code| \"\033[36m#{code}\033[0m\" } }, ''" 84 | ``` 85 | 86 | Most of them are 1) hard to read (Bash, Ruby one-liner), or 2) add dependencies to the project (language `X` libraries). 87 | 88 | Docker is de-facto standard tool for developers nowadays, so I don't think of it as of dependency. 89 | 90 | So I think my solution may be useful in real-world. Feel free to use it in your projects! 🖖 --------------------------------------------------------------------------------