diff --git a/Makefile.am b/Makefile.am
index d768de1492cd..ffbbd044050f 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -8,6 +8,8 @@ AM_CPPFLAGS = \
 	-include $(top_builddir)/config.h \
 	-DSYSCONFDIR=\""$(sysconfdir)"\" \
 	-DLIBEXECDIR=\""$(libexecdir)"\" \
+	-DPREFIX=\""$(prefix)"\" \
+	-DNDCTL_MAN_PATH=\""$(mandir)"\" \
 	-I${top_srcdir}/lib/ndctl \
 	-I${top_srcdir}/lib \
 	-I${top_srcdir}/ \
@@ -65,6 +67,7 @@ ndctl_SOURCES = ndctl.c \
 		builtin-xable-namespace.c \
 		builtin-xable-region.c \
 		builtin-test.c \
+		builtin-help.c \
 		builtin-zero-labels.c \
 		util/parse-options.c \
 		util/parse-options.h \
diff --git a/builtin-help.c b/builtin-help.c
new file mode 100644
index 000000000000..55d3114c4bf8
--- /dev/null
+++ b/builtin-help.c
@@ -0,0 +1,163 @@
+/*
+ * builtin-help.c
+ *
+ * Builtin help command
+ */
+#include <stdio.h>
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+#include <builtin.h>
+#include <util/strbuf.h>
+#include <util/parse-options.h>
+
+#define pr_err(x, ...) fprintf(stderr, x, ##__VA_ARGS__)
+#define STRERR_BUFSIZE  128     /* For the buffer size of strerror_r */
+
+static void exec_man_konqueror(const char *path, const char *page)
+{
+	const char *display = getenv("DISPLAY");
+
+	if (display && *display) {
+		struct strbuf man_page = STRBUF_INIT;
+		const char *filename = "kfmclient";
+		char sbuf[STRERR_BUFSIZE];
+
+		/* It's simpler to launch konqueror using kfmclient. */
+		if (path) {
+			const char *file = strrchr(path, '/');
+			if (file && !strcmp(file + 1, "konqueror")) {
+				char *new = strdup(path);
+				char *dest = strrchr(new, '/');
+
+				/* strlen("konqueror") == strlen("kfmclient") */
+				strcpy(dest + 1, "kfmclient");
+				path = new;
+			}
+			if (file)
+				filename = file;
+		} else
+			path = "kfmclient";
+		strbuf_addf(&man_page, "man:%s(1)", page);
+		execlp(path, filename, "newTab", man_page.buf, NULL);
+		warning("failed to exec '%s': %s", path,
+			strerror_r(errno, sbuf, sizeof(sbuf)));
+	}
+}
+
+static void exec_man_man(const char *path, const char *page)
+{
+	char sbuf[STRERR_BUFSIZE];
+
+	if (!path)
+		path = "man";
+	execlp(path, "man", page, NULL);
+	warning("failed to exec '%s': %s", path,
+		strerror_r(errno, sbuf, sizeof(sbuf)));
+}
+
+static char *cmd_to_page(const char *ndctl_cmd, char **page)
+{
+	int rc;
+
+	if (!ndctl_cmd)
+		rc = asprintf(page, "ndctl");
+	else if (!prefixcmp(ndctl_cmd, "ndctl"))
+		rc = asprintf(page, "%s", ndctl_cmd);
+	else
+		rc = asprintf(page, "ndctl-%s", ndctl_cmd);
+
+	if (rc < 0)
+		return NULL;
+	return *page;
+}
+
+static int is_absolute_path(const char *path)
+{
+	return path[0] == '/';
+}
+
+static const char *system_path(const char *path)
+{
+        static const char *prefix = PREFIX;
+        struct strbuf d = STRBUF_INIT;
+
+        if (is_absolute_path(path))
+                return path;
+
+        strbuf_addf(&d, "%s/%s", prefix, path);
+        path = strbuf_detach(&d, NULL);
+        return path;
+}
+
+static void setup_man_path(void)
+{
+	struct strbuf new_path = STRBUF_INIT;
+	const char *old_path = getenv("MANPATH");
+
+	/* We should always put ':' after our path. If there is no
+	 * old_path, the ':' at the end will let 'man' to try
+	 * system-wide paths after ours to find the manual page. If
+	 * there is old_path, we need ':' as delimiter. */
+	strbuf_addstr(&new_path, system_path(NDCTL_MAN_PATH));
+	strbuf_addch(&new_path, ':');
+	if (old_path)
+		strbuf_addstr(&new_path, old_path);
+
+	setenv("MANPATH", new_path.buf, 1);
+
+	strbuf_release(&new_path);
+}
+
+static void exec_viewer(const char *name, const char *page)
+{
+	if (!strcasecmp(name, "man"))
+		exec_man_man(NULL, page);
+	else if (!strcasecmp(name, "konqueror"))
+		exec_man_konqueror(NULL, page);
+	else
+		warning("'%s': unknown man viewer.", name);
+}
+
+static int show_man_page(const char *ndctl_cmd)
+{
+	const char *fallback = getenv("NDCTL_MAN_VIEWER");
+	char *page;
+
+	page = cmd_to_page(ndctl_cmd, &page);
+	if (!page)
+		return -1;
+	setup_man_path();
+	if (fallback)
+		exec_viewer(fallback, page);
+	exec_viewer("man", page);
+
+	pr_err("no man viewer handled the request");
+	free(page);
+	return -1;
+}
+
+int cmd_help(int argc, const char **argv)
+{
+	const char * const builtin_help_subcommands[] = {
+		"enable-region", "disable-region", "zero-labels",
+		"enable-namespace", "disable-namespace", NULL };
+	struct option builtin_help_options[] = {
+		OPT_END(),
+	};
+	const char *builtin_help_usage[] = {
+		"ndctl help [command]",
+		NULL
+	};
+
+	argc = parse_options_subcommand(argc, argv, builtin_help_options,
+			builtin_help_subcommands, builtin_help_usage, 0);
+
+	if (!argv[0]) {
+		printf("\n usage: %s\n\n", ndctl_usage_string);
+		printf("\n %s\n\n", ndctl_more_info_string);
+		return 0;
+	}
+
+	return show_man_page(argv[0]);
+}
diff --git a/builtin.h b/builtin.h
new file mode 100644
index 000000000000..ed0b7e497a43
--- /dev/null
+++ b/builtin.h
@@ -0,0 +1,26 @@
+#ifndef _NDCTL_BUILTIN_H_
+#define _NDCTL_BUILTIN_H_
+extern const char ndctl_usage_string[];
+extern const char ndctl_more_info_string[];
+
+struct cmd_struct {
+	const char *cmd;
+	int (*fn)(int, const char **);
+};
+
+int cmd_create_nfit(int argc, const char **argv);
+int cmd_enable_namespace(int argc, const char **argv);
+int cmd_disable_namespace(int argc, const char **argv);
+int cmd_enable_region(int argc, const char **argv);
+int cmd_disable_region(int argc, const char **argv);
+int cmd_zero_labels(int argc, const char **argv);
+int cmd_help(int argc, const char **argv);
+#ifdef ENABLE_TEST
+int cmd_test(int argc, const char **argv);
+#endif
+#ifdef ENABLE_DESTRUCTIVE
+int cmd_bat(int argc, const char **argv);
+#endif
+
+#endif /* _NDCTL_BUILTIN_H_ */
+
diff --git a/ndctl.c b/ndctl.c
index 242832af8249..f4e2746f85e3 100644
--- a/ndctl.c
+++ b/ndctl.c
@@ -5,46 +5,22 @@
 #include <unistd.h>
 #include <sys/stat.h>
 #include <sys/types.h>
+#include <builtin.h>
 #include <ccan/array_size/array_size.h>
 
 #include <util/strbuf.h>
 #include <util/util.h>
 
-static const char *ndctl_usage_string;
-
-static const char ndctl_more_info_string[] =
+const char ndctl_usage_string[] = "ndctl [--version] [--help] COMMAND [ARGS]";
+const char ndctl_more_info_string[] =
 	"See 'ndctl help COMMAND' for more information on a specific command.";
 
-struct cmd_struct {
-	const char *cmd;
-	int (*fn)(int, const char **);
-};
-
 static int cmd_version(int argc, const char **argv)
 {
 	printf("%s\n", VERSION);
 	return 0;
 }
 
-static int cmd_help(int argc, const char **argv)
-{
-	printf("\n%s\n\n", ndctl_usage_string);
-	return 0;
-}
-
-int cmd_create_nfit(int argc, const char **argv);
-int cmd_enable_namespace(int argc, const char **argv);
-int cmd_disable_namespace(int argc, const char **argv);
-int cmd_enable_region(int argc, const char **argv);
-int cmd_disable_region(int argc, const char **argv);
-int cmd_zero_labels(int argc, const char **argv);
-#ifdef ENABLE_TEST
-int cmd_test(int argc, const char **argv);
-#endif
-#ifdef ENABLE_DESTRUCTIVE
-int cmd_bat(int argc, const char **argv);
-#endif
-
 static struct cmd_struct commands[] = {
 	{ "version", cmd_version },
 	{ "create-nfit", cmd_create_nfit },
@@ -53,6 +29,7 @@ static struct cmd_struct commands[] = {
 	{ "enable-region", cmd_enable_region },
 	{ "disable-region", cmd_disable_region },
 	{ "zero-labels", cmd_zero_labels },
+	{ "help", cmd_help },
 	#ifdef ENABLE_TEST
 	{ "test", cmd_test },
 	#endif
@@ -61,29 +38,6 @@ static struct cmd_struct commands[] = {
 	#endif
 };
 
-/* place holder until help system is implemented */
-static char *init_usage_string(void)
-{
-	char *def = "ndctl [--version] [--help] COMMAND [ARGS]";
-	unsigned int len = strlen(def) + 1, i, p;
-	char *u;
-
-	for (i = 0; i < ARRAY_SIZE(commands); i++)
-		len += strlen(commands[i].cmd) + 2;
-	u = calloc(1, len);
-	if (!u)
-		return def;
-
-	p = sprintf(u, "%s", "ndctl [--version] [--help] ");
-	for (i = 0; i < ARRAY_SIZE(commands); i++) {
-		p += sprintf(&u[p], "%s", commands[i].cmd);
-		if ((i + 1) < ARRAY_SIZE(commands))
-			p += sprintf(&u[p], "|");
-	}
-	p = sprintf(&u[p], "%s", " [ARGS]");
-	return u;
-}
-
 static int handle_options(const char ***argv, int *argc)
 {
 	int handled = 0;
@@ -93,11 +47,22 @@ static int handle_options(const char ***argv, int *argc)
 		if (cmd[0] != '-')
 			break;
 
-               if (!strcmp(cmd, "--help"))
-			exit(cmd_help(*argc, *argv));
+		if (!strcmp(cmd, "--version") || !strcmp(cmd, "--help"))
+			break;
 
-		if (!strcmp(cmd, "--version"))
+		/*
+		 * Shortcut for '-h' and '-v' options to invoke help
+		 * and version command.
+		 */
+		if (!strcmp(cmd, "-h")) {
+			(*argv)[0] = "--help";
 			break;
+		}
+
+		if (!strcmp(cmd, "-v")) {
+			(*argv)[0] = "--version";
+			break;
+		}
 
 		if (!strcmp(cmd, "--list-cmds")) {
 			unsigned int i;
@@ -160,6 +125,12 @@ static void handle_internal_command(int argc, const char **argv)
 	const char *cmd = argv[0];
 	unsigned int i;
 
+	/* Turn "ndctl cmd --help" into "ndctl help cmd" */
+	if (argc > 1 && !strcmp(argv[1], "--help")) {
+		argv[1] = argv[0];
+		argv[0] = cmd = "help";
+	}
+
 	for (i = 0; i < ARRAY_SIZE(commands); i++) {
 		struct cmd_struct *p = commands+i;
 		if (strcmp(p->cmd, cmd))
@@ -170,8 +141,6 @@ static void handle_internal_command(int argc, const char **argv)
 
 int main(int argc, const char **argv)
 {
-	ndctl_usage_string = init_usage_string();
-
 	/* Look for flags.. */
 	argv++;
 	argc--;
@@ -183,7 +152,7 @@ int main(int argc, const char **argv)
 	} else {
 		/* The user didn't specify a command; give them help */
 		printf("\n usage: %s\n\n", ndctl_usage_string);
-		/* printf("\n %s\n\n", ndctl_more_info_string); TODO */
+		printf("\n %s\n\n", ndctl_more_info_string);
 		goto out;
 	}
 	handle_internal_command(argc, argv);
