about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEelco Dolstra <eelco.dolstra@logicblox.com>2016-03-04T14·21+0100
committerEelco Dolstra <eelco.dolstra@logicblox.com>2016-03-04T14·21+0100
commit7c9d7a253c52dfbf8488d3394fe8af104a3af234 (patch)
treeb78f69734d35f428456dde41a87808feead701e0
parent0a26b56cba5d9f1fa815273fadc500284dda1118 (diff)
parent1b4b16cc6d9585c7bbeb871edc815137baef8f83 (diff)
Merge branch 'new-cli'
-rw-r--r--.gitignore3
-rw-r--r--Makefile2
-rw-r--r--src/boost/format/exceptions.hpp10
-rw-r--r--src/libmain/common-args.cc38
-rw-r--r--src/libmain/common-args.hh23
-rw-r--r--src/libmain/shared.cc148
-rw-r--r--src/libmain/shared.hh3
-rw-r--r--src/libstore/http-binary-cache-store.cc76
-rw-r--r--src/libstore/local-binary-cache-store.cc14
-rw-r--r--src/libstore/store-api.hh5
-rw-r--r--src/libutil/args.cc179
-rw-r--r--src/libutil/args.hh162
-rw-r--r--src/nix-hash/local.mk7
-rw-r--r--src/nix-hash/nix-hash.cc63
-rw-r--r--src/nix/build.cc46
-rw-r--r--src/nix/cat.cc74
-rw-r--r--src/nix/command.cc65
-rw-r--r--src/nix/command.hh59
-rw-r--r--src/nix/hash.cc140
-rw-r--r--src/nix/installables.cc75
-rw-r--r--src/nix/installables.hh38
-rw-r--r--src/nix/legacy.cc7
-rw-r--r--src/nix/legacy.hh22
-rw-r--r--src/nix/local.mk9
-rw-r--r--src/nix/ls.cc123
-rw-r--r--src/nix/main.cc55
26 files changed, 1285 insertions, 161 deletions
diff --git a/.gitignore b/.gitignore
index 94067256b8bc..de8e9354fbbe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -73,9 +73,6 @@ Makefile.config
 # /src/nix-env/
 /src/nix-env/nix-env
 
-# /src/nix-hash/
-/src/nix-hash/nix-hash
-
 # /src/nix-instantiate/
 /src/nix-instantiate/nix-instantiate
 
diff --git a/Makefile b/Makefile
index 3a204de888cc..39870af75104 100644
--- a/Makefile
+++ b/Makefile
@@ -5,7 +5,7 @@ makefiles = \
   src/libstore/local.mk \
   src/libmain/local.mk \
   src/libexpr/local.mk \
-  src/nix-hash/local.mk \
+  src/nix/local.mk \
   src/nix-store/local.mk \
   src/nix-instantiate/local.mk \
   src/nix-env/local.mk \
diff --git a/src/boost/format/exceptions.hpp b/src/boost/format/exceptions.hpp
index 79e452449ef8..a7641458c95e 100644
--- a/src/boost/format/exceptions.hpp
+++ b/src/boost/format/exceptions.hpp
@@ -33,7 +33,7 @@ namespace io {
 class format_error : public std::exception
 {
 public:
-  format_error() {}
+  format_error() { abort(); }
   virtual const char *what() const throw()
   {
     return "boost::format_error: "
@@ -44,7 +44,7 @@ public:
 class bad_format_string : public format_error
 {
 public:
-  bad_format_string() {}
+  bad_format_string() { abort(); }
   virtual const char *what() const throw()
   {
     return "boost::bad_format_string: "
@@ -55,7 +55,7 @@ public:
 class too_few_args : public format_error
 {
 public:
-  too_few_args() {}
+  too_few_args() { abort(); }
   virtual const char *what() const throw()
   {
     return "boost::too_few_args: "
@@ -66,7 +66,7 @@ public:
 class too_many_args : public format_error
 {
 public:
-  too_many_args() {}
+  too_many_args() { abort(); }
   virtual const char *what() const throw()
   {
     return "boost::too_many_args: "
@@ -78,7 +78,7 @@ public:
 class  out_of_range : public format_error
 {
 public:
-  out_of_range() {}
+  out_of_range() { abort(); }
   virtual const char *what() const throw()
   {
     return "boost::out_of_range: "
diff --git a/src/libmain/common-args.cc b/src/libmain/common-args.cc
new file mode 100644
index 000000000000..9219f380c74f
--- /dev/null
+++ b/src/libmain/common-args.cc
@@ -0,0 +1,38 @@
+#include "common-args.hh"
+#include "globals.hh"
+
+namespace nix {
+
+MixCommonArgs::MixCommonArgs(const string & programName)
+    : programName(programName)
+{
+    mkFlag('v', "verbose", "increase verbosity level", []() {
+        verbosity = (Verbosity) (verbosity + 1);
+    });
+
+    mkFlag(0, "quiet", "decrease verbosity level", []() {
+        verbosity = verbosity > lvlError ? (Verbosity) (verbosity - 1) : lvlError;
+    });
+
+    mkFlag(0, "debug", "enable debug output", []() {
+        verbosity = lvlDebug;
+    });
+
+    mkFlag1(0, "log-type", "type", "set logging format ('pretty', 'flat', 'systemd')",
+        [](std::string s) {
+            if (s == "pretty") logType = ltPretty;
+            else if (s == "escapes") logType = ltEscapes;
+            else if (s == "flat") logType = ltFlat;
+            else if (s == "systemd") logType = ltSystemd;
+            else throw UsageError("unknown log type");
+        });
+
+    mkFlag(0, "option", {"name", "value"}, "set a Nix configuration option (overriding nix.conf)", 2,
+        [](Strings ss) {
+            auto name = ss.front(); ss.pop_front();
+            auto value = ss.front();
+            settings.set(name, value);
+        });
+}
+
+}
diff --git a/src/libmain/common-args.hh b/src/libmain/common-args.hh
new file mode 100644
index 000000000000..2c0d71edd815
--- /dev/null
+++ b/src/libmain/common-args.hh
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "args.hh"
+
+namespace nix {
+
+struct MixCommonArgs : virtual Args
+{
+    string programName;
+    MixCommonArgs(const string & programName);
+};
+
+struct MixDryRun : virtual Args
+{
+    bool dryRun;
+
+    MixDryRun()
+    {
+        mkFlag(0, "dry-run", "show what this command would do without doing it", &dryRun);
+    }
+};
+
+}
diff --git a/src/libmain/shared.cc b/src/libmain/shared.cc
index c27302227304..e883967b71a1 100644
--- a/src/libmain/shared.cc
+++ b/src/libmain/shared.cc
@@ -1,7 +1,8 @@
 #include "config.h"
 
-#include "shared.hh"
+#include "common-args.hh"
 #include "globals.hh"
+#include "shared.hh"
 #include "store-api.hh"
 #include "util.hh"
 
@@ -85,16 +86,6 @@ void printMissing(ref<Store> store, const PathSet & willBuild,
 }
 
 
-static void setLogType(string lt)
-{
-    if (lt == "pretty") logType = ltPretty;
-    else if (lt == "escapes") logType = ltEscapes;
-    else if (lt == "flat") logType = ltFlat;
-    else if (lt == "systemd") logType = ltSystemd;
-    else throw UsageError("unknown log type");
-}
-
-
 string getArg(const string & opt,
     Strings::iterator & i, const Strings::iterator & end)
 {
@@ -181,77 +172,80 @@ void initNix()
 }
 
 
-void parseCmdLine(int argc, char * * argv,
-    std::function<bool(Strings::iterator & arg, const Strings::iterator & end)> parseArg)
+struct LegacyArgs : public MixCommonArgs
 {
-    /* Put the arguments in a vector. */
-    Strings args;
-    argc--; argv++;
-    while (argc--) args.push_back(*argv++);
-
-    /* Process default options. */
-    for (Strings::iterator i = args.begin(); i != args.end(); ++i) {
-        string arg = *i;
-
-        /* Expand compound dash options (i.e., `-qlf' -> `-q -l -f'). */
-        if (arg.length() > 2 && arg[0] == '-' && arg[1] != '-' && isalpha(arg[1])) {
-            *i = (string) "-" + arg[1];
-            auto next = i; ++next;
-            for (unsigned int j = 2; j < arg.length(); j++)
-                if (isalpha(arg[j]))
-                    args.insert(next, (string) "-" + arg[j]);
-                else {
-                    args.insert(next, string(arg, j));
-                    break;
-                }
-            arg = *i;
-        }
+    std::function<bool(Strings::iterator & arg, const Strings::iterator & end)> parseArg;
 
-        if (arg == "--verbose" || arg == "-v") verbosity = (Verbosity) (verbosity + 1);
-        else if (arg == "--quiet") verbosity = verbosity > lvlError ? (Verbosity) (verbosity - 1) : lvlError;
-        else if (arg == "--log-type") {
-            string s = getArg(arg, i, args.end());
-            setLogType(s);
-        }
-        else if (arg == "--no-build-output" || arg == "-Q")
-            settings.buildVerbosity = lvlVomit;
-        else if (arg == "--print-build-trace")
-            settings.printBuildTrace = true;
-        else if (arg == "--keep-failed" || arg == "-K")
-            settings.keepFailed = true;
-        else if (arg == "--keep-going" || arg == "-k")
-            settings.keepGoing = true;
-        else if (arg == "--fallback")
+    LegacyArgs(const std::string & programName,
+        std::function<bool(Strings::iterator & arg, const Strings::iterator & end)> parseArg)
+        : MixCommonArgs(programName), parseArg(parseArg)
+    {
+        mkFlag('Q', "no-build-output", "do not show build output",
+            &settings.buildVerbosity, lvlVomit);
+
+        mkFlag(0, "print-build-trace", "emit special build trace message",
+            &settings.printBuildTrace);
+
+        mkFlag('K', "keep-failed", "keep temporary directories of failed builds",
+            &settings.keepFailed);
+
+        mkFlag('k', "keep-going", "keep going after a build fails",
+            &settings.keepGoing);
+
+        mkFlag(0, "fallback", "build from source if substitution fails", []() {
             settings.set("build-fallback", "true");
-        else if (arg == "--max-jobs" || arg == "-j")
-            settings.set("build-max-jobs", getArg(arg, i, args.end()));
-        else if (arg == "--cores")
-            settings.set("build-cores", getArg(arg, i, args.end()));
-        else if (arg == "--readonly-mode")
-            settings.readOnlyMode = true;
-        else if (arg == "--max-silent-time")
-            settings.set("build-max-silent-time", getArg(arg, i, args.end()));
-        else if (arg == "--timeout")
-            settings.set("build-timeout", getArg(arg, i, args.end()));
-        else if (arg == "--no-build-hook")
-            settings.useBuildHook = false;
-        else if (arg == "--show-trace")
-            settings.showTrace = true;
-        else if (arg == "--no-gc-warning")
-            gcWarning = false;
-        else if (arg == "--option") {
-            ++i; if (i == args.end()) throw UsageError("‘--option’ requires two arguments");
-            string name = *i;
-            ++i; if (i == args.end()) throw UsageError("‘--option’ requires two arguments");
-            string value = *i;
-            settings.set(name, value);
-        }
-        else {
-            if (!parseArg(i, args.end()))
-                throw UsageError(format("unrecognised option ‘%1%’") % *i);
-        }
+        });
+
+        auto intSettingAlias = [&](char shortName, const std::string & longName,
+            const std::string & description, const std::string & dest) {
+            mkFlag<unsigned int>(shortName, longName, description, [=](unsigned int n) {
+                settings.set(dest, std::to_string(n));
+            });
+        };
+
+        intSettingAlias('j', "max-jobs", "maximum number of parallel builds", "build-max-jobs");
+        intSettingAlias(0, "cores", "maximum number of CPU cores to use inside a build", "build-cores");
+        intSettingAlias(0, "max-silent-time", "number of seconds of silence before a build is killed", "build-max-silent-time");
+        intSettingAlias(0, "timeout", "number of seconds before a build is killed", "build-timeout");
+
+        mkFlag(0, "readonly-mode", "do not write to the Nix store",
+            &settings.readOnlyMode);
+
+        mkFlag(0, "no-build-hook", "disable use of the build hook mechanism",
+            &settings.useBuildHook, false);
+
+        mkFlag(0, "show-trace", "show Nix expression stack trace in evaluation errors",
+            &settings.showTrace);
+
+        mkFlag(0, "no-gc-warning", "disable warning about not using ‘--add-root’",
+            &gcWarning, false);
     }
 
+    bool processFlag(Strings::iterator & pos, Strings::iterator end) override
+    {
+        if (MixCommonArgs::processFlag(pos, end)) return true;
+        bool res = parseArg(pos, end);
+        if (res) ++pos;
+        return res;
+    }
+
+    bool processArgs(const Strings & args, bool finish) override
+    {
+        if (args.empty()) return true;
+        assert(args.size() == 1);
+        Strings ss(args);
+        auto pos = ss.begin();
+        if (!parseArg(pos, ss.end()))
+            throw UsageError(format("unexpected argument ‘%1%’") % args.front());
+        return true;
+    }
+};
+
+
+void parseCmdLine(int argc, char * * argv,
+    std::function<bool(Strings::iterator & arg, const Strings::iterator & end)> parseArg)
+{
+    LegacyArgs(baseNameOf(argv[0]), parseArg).parseCmdline(argvToStrings(argc, argv));
     settings.update();
 }
 
diff --git a/src/libmain/shared.hh b/src/libmain/shared.hh
index 0682267fa376..6d94a22f788e 100644
--- a/src/libmain/shared.hh
+++ b/src/libmain/shared.hh
@@ -1,6 +1,7 @@
 #pragma once
 
 #include "util.hh"
+#include "args.hh"
 
 #include <signal.h>
 
@@ -9,8 +10,6 @@
 
 namespace nix {
 
-MakeError(UsageError, nix::Error);
-
 class Exit : public std::exception
 {
 public:
diff --git a/src/libstore/http-binary-cache-store.cc b/src/libstore/http-binary-cache-store.cc
new file mode 100644
index 000000000000..78f4497e7665
--- /dev/null
+++ b/src/libstore/http-binary-cache-store.cc
@@ -0,0 +1,76 @@
+#include "binary-cache-store.hh"
+#include "download.hh"
+
+namespace nix {
+
+class HttpBinaryCacheStore : public BinaryCacheStore
+{
+private:
+
+    Path cacheUri;
+
+    ref<Downloader> downloader;
+
+public:
+
+    HttpBinaryCacheStore(std::shared_ptr<Store> localStore,
+        const Path & secretKeyFile, const Path & publicKeyFile,
+        const Path & _cacheUri)
+        : BinaryCacheStore(localStore, secretKeyFile, publicKeyFile)
+        , cacheUri(_cacheUri)
+        , downloader(makeDownloader())
+    {
+        if (cacheUri.back() == '/')
+            cacheUri.pop_back();
+    }
+
+    void init() override
+    {
+        // FIXME: do this lazily?
+        if (!fileExists("nix-cache-info"))
+            throw Error(format("‘%s’ does not appear to be a binary cache") % cacheUri);
+    }
+
+protected:
+
+    bool fileExists(const std::string & path) override
+    {
+        try {
+            DownloadOptions options;
+            options.showProgress = DownloadOptions::no;
+            options.head = true;
+            downloader->download(cacheUri + "/" + path, options);
+            return true;
+        } catch (DownloadError & e) {
+            if (e.error == Downloader::NotFound)
+                return false;
+            throw;
+        }
+    }
+
+    void upsertFile(const std::string & path, const std::string & data)
+    {
+        throw Error("uploading to an HTTP binary cache is not supported");
+    }
+
+    std::string getFile(const std::string & path) override
+    {
+        DownloadOptions options;
+        options.showProgress = DownloadOptions::no;
+        return downloader->download(cacheUri + "/" + path, options).data;
+    }
+
+};
+
+static RegisterStoreImplementation regStore([](const std::string & uri) -> std::shared_ptr<Store> {
+    if (std::string(uri, 0, 7) != "http://" &&
+        std::string(uri, 0, 8) != "https://") return 0;
+    auto store = std::make_shared<HttpBinaryCacheStore>(std::shared_ptr<Store>(0),
+        "", "", // FIXME: allow the signing key to be set
+        uri);
+    store->init();
+    return store;
+});
+
+}
+
diff --git a/src/libstore/local-binary-cache-store.cc b/src/libstore/local-binary-cache-store.cc
index a10c9d1069d5..8590aea185d4 100644
--- a/src/libstore/local-binary-cache-store.cc
+++ b/src/libstore/local-binary-cache-store.cc
@@ -65,13 +65,21 @@ std::string LocalBinaryCacheStore::getFile(const std::string & path)
     return readFile(binaryCacheDir + "/" + path);
 }
 
+ref<Store> openLocalBinaryCacheStore(std::shared_ptr<Store> localStore,
+    const Path & secretKeyFile, const Path & publicKeyFile,
+    const Path & binaryCacheDir)
+{
+    auto store = std::make_shared<LocalBinaryCacheStore>(
+        localStore, secretKeyFile, publicKeyFile, binaryCacheDir);
+    store->init();
+    return ref<Store>(std::shared_ptr<Store>(store));
+}
+
 static RegisterStoreImplementation regStore([](const std::string & uri) -> std::shared_ptr<Store> {
     if (std::string(uri, 0, 7) != "file://") return 0;
-    auto store = std::make_shared<LocalBinaryCacheStore>(std::shared_ptr<Store>(0),
+    return openLocalBinaryCacheStore(std::shared_ptr<Store>(0),
         "", "", // FIXME: allow the signing key to be set
         std::string(uri, 7));
-    store->init();
-    return store;
 });
 
 }
diff --git a/src/libstore/store-api.hh b/src/libstore/store-api.hh
index 97e834ed2bd8..9825d45db102 100644
--- a/src/libstore/store-api.hh
+++ b/src/libstore/store-api.hh
@@ -453,6 +453,11 @@ ref<Store> openStoreAt(const std::string & uri);
 ref<Store> openStore();
 
 
+ref<Store> openLocalBinaryCacheStore(std::shared_ptr<Store> localStore,
+    const Path & secretKeyFile, const Path & publicKeyFile,
+    const Path & binaryCacheDir);
+
+
 /* Store implementation registration. */
 typedef std::function<std::shared_ptr<Store>(const std::string & uri)> OpenStore;
 
diff --git a/src/libutil/args.cc b/src/libutil/args.cc
new file mode 100644
index 000000000000..6e4b82a279ce
--- /dev/null
+++ b/src/libutil/args.cc
@@ -0,0 +1,179 @@
+#include "args.hh"
+#include "hash.hh"
+
+namespace nix {
+
+void Args::parseCmdline(const Strings & _cmdline)
+{
+    Strings pendingArgs;
+    bool dashDash = false;
+
+    Strings cmdline(_cmdline);
+
+    for (auto pos = cmdline.begin(); pos != cmdline.end(); ) {
+
+        auto arg = *pos;
+
+        /* Expand compound dash options (i.e., `-qlf' -> `-q -l -f',
+           `-j3` -> `-j 3`). */
+        if (!dashDash && arg.length() > 2 && arg[0] == '-' && arg[1] != '-' && isalpha(arg[1])) {
+            *pos = (string) "-" + arg[1];
+            auto next = pos; ++next;
+            for (unsigned int j = 2; j < arg.length(); j++)
+                if (isalpha(arg[j]))
+                    cmdline.insert(next, (string) "-" + arg[j]);
+                else {
+                    cmdline.insert(next, string(arg, j));
+                    break;
+                }
+            arg = *pos;
+        }
+
+        if (!dashDash && arg == "--") {
+            dashDash = true;
+            ++pos;
+        }
+        else if (!dashDash && std::string(arg, 0, 1) == "-") {
+            if (!processFlag(pos, cmdline.end()))
+                throw UsageError(format("unrecognised flag ‘%1%’") % arg);
+        }
+        else {
+            pendingArgs.push_back(*pos++);
+            if (processArgs(pendingArgs, false))
+                pendingArgs.clear();
+        }
+    }
+
+    processArgs(pendingArgs, true);
+}
+
+void Args::printHelp(const string & programName, std::ostream & out)
+{
+    std::cout << "Usage: " << programName << " <FLAGS>...";
+    for (auto & exp : expectedArgs) {
+        std::cout << renderLabels({exp.label});
+        // FIXME: handle arity > 1
+        if (exp.arity == 0) std::cout << "...";
+    }
+    std::cout << "\n";
+
+    auto s = description();
+    if (s != "")
+        std::cout << "\nSummary: " << s << ".\n";
+
+    if (longFlags.size()) {
+        std::cout << "\n";
+        std::cout << "Flags:\n";
+        printFlags(out);
+    }
+}
+
+void Args::printFlags(std::ostream & out)
+{
+    Table2 table;
+    for (auto & flags : longFlags)
+        table.push_back(std::make_pair(
+                "--" + flags.first + renderLabels(flags.second.labels),
+                flags.second.description));
+    printTable(out, table);
+}
+
+bool Args::processFlag(Strings::iterator & pos, Strings::iterator end)
+{
+    assert(pos != end);
+
+    auto process = [&](const std::string & name, const Flag & flag) -> bool {
+        ++pos;
+        Strings args;
+        for (size_t n = 0 ; n < flag.arity; ++n) {
+            if (pos == end)
+                throw UsageError(format("flag ‘%1%’ requires %2% argument(s)")
+                    % name % flag.arity);
+            args.push_back(*pos++);
+        }
+        flag.handler(args);
+        return true;
+    };
+
+    if (string(*pos, 0, 2) == "--") {
+        auto i = longFlags.find(string(*pos, 2));
+        if (i == longFlags.end()) return false;
+        return process("--" + i->first, i->second);
+    }
+
+    if (string(*pos, 0, 1) == "-" && pos->size() == 2) {
+        auto c = (*pos)[1];
+        auto i = shortFlags.find(c);
+        if (i == shortFlags.end()) return false;
+        return process(std::string("-") + c, i->second);
+    }
+
+    return false;
+}
+
+bool Args::processArgs(const Strings & args, bool finish)
+{
+    if (expectedArgs.empty()) {
+        if (!args.empty())
+            throw UsageError(format("unexpected argument ‘%1%’") % args.front());
+        return true;
+    }
+
+    auto & exp = expectedArgs.front();
+
+    bool res = false;
+
+    if ((exp.arity == 0 && finish) ||
+        (exp.arity > 0 && args.size() == exp.arity))
+    {
+        exp.handler(args);
+        expectedArgs.pop_front();
+        res = true;
+    }
+
+    if (finish && !expectedArgs.empty())
+        throw UsageError("more arguments are required");
+
+    return res;
+}
+
+void Args::mkHashTypeFlag(const std::string & name, HashType * ht)
+{
+    mkFlag1(0, name, "TYPE", "hash algorithm (‘md5’, ‘sha1’, ‘sha256’, or ‘sha512’)", [=](std::string s) {
+        *ht = parseHashType(s);
+        if (*ht == htUnknown)
+            throw UsageError(format("unknown hash type ‘%1%’") % s);
+    });
+}
+
+Strings argvToStrings(int argc, char * * argv)
+{
+    Strings args;
+    argc--; argv++;
+    while (argc--) args.push_back(*argv++);
+    return args;
+}
+
+std::string renderLabels(const Strings & labels)
+{
+    std::string res;
+    for (auto label : labels) {
+        for (auto & c : label) c = std::toupper(c);
+        res += " <" + label + ">";
+    }
+    return res;
+}
+
+void printTable(std::ostream & out, const Table2 & table)
+{
+    size_t max = 0;
+    for (auto & row : table)
+        max = std::max(max, row.first.size());
+    for (auto & row : table) {
+        out << "  " << row.first
+            << std::string(max - row.first.size() + 2, ' ')
+            << row.second << "\n";
+    }
+}
+
+}
diff --git a/src/libutil/args.hh b/src/libutil/args.hh
new file mode 100644
index 000000000000..4469a046d28a
--- /dev/null
+++ b/src/libutil/args.hh
@@ -0,0 +1,162 @@
+#pragma once
+
+#include <iostream>
+#include <map>
+#include <memory>
+
+#include "util.hh"
+
+namespace nix {
+
+MakeError(UsageError, nix::Error);
+
+enum HashType : char;
+
+class Args
+{
+public:
+
+    /* Parse the command line, throwing a UsageError if something goes
+       wrong. */
+    void parseCmdline(const Strings & cmdline);
+
+    virtual void printHelp(const string & programName, std::ostream & out);
+
+    virtual std::string description() { return ""; }
+
+protected:
+
+    /* Flags. */
+    struct Flag
+    {
+        std::string description;
+        Strings labels;
+        size_t arity;
+        std::function<void(Strings)> handler;
+    };
+
+    std::map<std::string, Flag> longFlags;
+    std::map<char, Flag> shortFlags;
+
+    virtual bool processFlag(Strings::iterator & pos, Strings::iterator end);
+
+    void printFlags(std::ostream & out);
+
+    /* Positional arguments. */
+    struct ExpectedArg
+    {
+        std::string label;
+        size_t arity; // 0 = any
+        std::function<void(Strings)> handler;
+    };
+
+    std::list<ExpectedArg> expectedArgs;
+
+    virtual bool processArgs(const Strings & args, bool finish);
+
+public:
+
+    /* Helper functions for constructing flags / positional
+       arguments. */
+
+    void mkFlag(char shortName, const std::string & longName,
+        const Strings & labels, const std::string & description,
+        size_t arity, std::function<void(Strings)> handler)
+    {
+        auto flag = Flag{description, labels, arity, handler};
+        if (shortName) shortFlags[shortName] = flag;
+        longFlags[longName] = flag;
+    }
+
+    void mkFlag(char shortName, const std::string & longName,
+        const std::string & description, std::function<void()> fun)
+    {
+        mkFlag(shortName, longName, {}, description, 0, std::bind(fun));
+    }
+
+    void mkFlag1(char shortName, const std::string & longName,
+        const std::string & label, const std::string & description,
+        std::function<void(std::string)> fun)
+    {
+        mkFlag(shortName, longName, {label}, description, 1, [=](Strings ss) {
+            fun(ss.front());
+        });
+    }
+
+    void mkFlag(char shortName, const std::string & name,
+        const std::string & description, bool * dest)
+    {
+        mkFlag(shortName, name, description, dest, true);
+    }
+
+    void mkFlag(char shortName, const std::string & longName,
+        const std::string & label, const std::string & description,
+        string * dest)
+    {
+        mkFlag1(shortName, longName, label, description, [=](std::string s) {
+            *dest = s;
+        });
+    }
+
+    void mkHashTypeFlag(const std::string & name, HashType * ht);
+
+    template<class T>
+    void mkFlag(char shortName, const std::string & longName, const std::string & description,
+        T * dest, const T & value)
+    {
+        mkFlag(shortName, longName, {}, description, 0, [=](Strings ss) {
+            *dest = value;
+        });
+    }
+
+    template<class I>
+    void mkIntFlag(char shortName, const std::string & longName,
+        const std::string & description, I * dest)
+    {
+        mkFlag<I>(shortName, longName, description, [=](I n) {
+            *dest = n;
+        });
+    }
+
+    template<class I>
+    void mkFlag(char shortName, const std::string & longName,
+        const std::string & description, std::function<void(I)> fun)
+    {
+        mkFlag(shortName, longName, {"N"}, description, 1, [=](Strings ss) {
+            I n;
+            if (!string2Int(ss.front(), n))
+                throw UsageError(format("flag ‘--%1%’ requires a integer argument") % longName);
+            fun(n);
+        });
+    }
+
+    /* Expect a string argument. */
+    void expectArg(const std::string & label, string * dest)
+    {
+        expectedArgs.push_back(ExpectedArg{label, 1, [=](Strings ss) {
+            *dest = ss.front();
+        }});
+    }
+
+    /* Expect 0 or more arguments. */
+    void expectArgs(const std::string & label, Strings * dest)
+    {
+        expectedArgs.push_back(ExpectedArg{label, 0, [=](Strings ss) {
+            *dest = ss;
+        }});
+    }
+
+    friend class MultiCommand;
+};
+
+Strings argvToStrings(int argc, char * * argv);
+
+/* Helper function for rendering argument labels. */
+std::string renderLabels(const Strings & labels);
+
+/* Helper function for printing 2-column tables. */
+typedef std::vector<std::pair<std::string, std::string>> Table2;
+
+void printTable(std::ostream & out, const Table2 & table);
+
+}
diff --git a/src/nix-hash/local.mk b/src/nix-hash/local.mk
deleted file mode 100644
index 7c290ca8466e..000000000000
--- a/src/nix-hash/local.mk
+++ /dev/null
@@ -1,7 +0,0 @@
-programs += nix-hash
-
-nix-hash_DIR := $(d)
-
-nix-hash_SOURCES := $(d)/nix-hash.cc
-
-nix-hash_LIBS = libmain libstore libutil libformat
diff --git a/src/nix-hash/nix-hash.cc b/src/nix-hash/nix-hash.cc
deleted file mode 100644
index 8035162aea37..000000000000
--- a/src/nix-hash/nix-hash.cc
+++ /dev/null
@@ -1,63 +0,0 @@
-#include "hash.hh"
-#include "shared.hh"
-
-#include <iostream>
-
-using namespace nix;
-
-
-int main(int argc, char * * argv)
-{
-    HashType ht = htMD5;
-    bool flat = false;
-    bool base32 = false;
-    bool truncate = false;
-    enum { opHash, opTo32, opTo16 } op = opHash;
-
-    Strings ss;
-
-    return handleExceptions(argv[0], [&]() {
-        initNix();
-
-        parseCmdLine(argc, argv, [&](Strings::iterator & arg, const Strings::iterator & end) {
-            if (*arg == "--help")
-                showManPage("nix-hash");
-            else if (*arg == "--version")
-                printVersion("nix-hash");
-            else if (*arg == "--flat") flat = true;
-            else if (*arg == "--base32") base32 = true;
-            else if (*arg == "--truncate") truncate = true;
-            else if (*arg == "--type") {
-                string s = getArg(*arg, arg, end);
-                ht = parseHashType(s);
-                if (ht == htUnknown)
-                    throw UsageError(format("unknown hash type ‘%1%’") % s);
-            }
-            else if (*arg == "--to-base16") op = opTo16;
-            else if (*arg == "--to-base32") op = opTo32;
-            else if (*arg != "" && arg->at(0) == '-')
-                return false;
-            else
-                ss.push_back(*arg);
-            return true;
-        });
-
-        if (op == opHash) {
-            for (auto & i : ss) {
-                Hash h = flat ? hashFile(ht, i) : hashPath(ht, i).first;
-                if (truncate && h.hashSize > 20) h = compressHash(h, 20);
-                std::cout << format("%1%\n") %
-                    (base32 ? printHash32(h) : printHash(h));
-            }
-        }
-
-        else {
-            for (auto & i : ss) {
-                Hash h = parseHash16or32(ht, i);
-                std::cout << format("%1%\n") %
-                    (op == opTo16 ? printHash(h) : printHash32(h));
-            }
-        }
-    });
-}
-
diff --git a/src/nix/build.cc b/src/nix/build.cc
new file mode 100644
index 000000000000..812464d7582b
--- /dev/null
+++ b/src/nix/build.cc
@@ -0,0 +1,46 @@
+#include "command.hh"
+#include "common-args.hh"
+#include "installables.hh"
+#include "shared.hh"
+#include "store-api.hh"
+
+using namespace nix;
+
+struct CmdBuild : StoreCommand, MixDryRun, MixInstallables
+{
+    CmdBuild()
+    {
+    }
+
+    std::string name() override
+    {
+        return "build";
+    }
+
+    std::string description() override
+    {
+        return "build a derivation or fetch a store path";
+    }
+
+    void run(ref<Store> store) override
+    {
+        auto elems = evalInstallables(store);
+
+        PathSet pathsToBuild;
+
+        for (auto & elem : elems) {
+            if (elem.isDrv)
+                pathsToBuild.insert(elem.drvPath);
+            else
+                pathsToBuild.insert(elem.outPaths.begin(), elem.outPaths.end());
+        }
+
+        printMissing(store, pathsToBuild);
+
+        if (dryRun) return;
+
+        store->buildPaths(pathsToBuild);
+    }
+};
+
+static RegisterCommand r1(make_ref<CmdBuild>());
diff --git a/src/nix/cat.cc b/src/nix/cat.cc
new file mode 100644
index 000000000000..2405a8cb44ef
--- /dev/null
+++ b/src/nix/cat.cc
@@ -0,0 +1,74 @@
+#include "command.hh"
+#include "store-api.hh"
+#include "fs-accessor.hh"
+#include "nar-accessor.hh"
+
+using namespace nix;
+
+struct MixCat : virtual Args
+{
+    std::string path;
+
+    void cat(ref<FSAccessor> accessor)
+    {
+        auto st = accessor->stat(path);
+        if (st.type == FSAccessor::Type::tMissing)
+            throw Error(format("path ‘%1%’ does not exist") % path);
+        if (st.type != FSAccessor::Type::tRegular)
+            throw Error(format("path ‘%1%’ is not a regular file") % path);
+
+        std::cout << accessor->readFile(path);
+    }
+};
+
+struct CmdCatStore : StoreCommand, MixCat
+{
+    CmdCatStore()
+    {
+        expectArg("path", &path);
+    }
+
+    std::string name() override
+    {
+        return "cat-store";
+    }
+
+    std::string description() override
+    {
+        return "print the contents of a store file on stdout";
+    }
+
+    void run(ref<Store> store) override
+    {
+        cat(store->getFSAccessor());
+    }
+};
+
+struct CmdCatNar : StoreCommand, MixCat
+{
+    Path narPath;
+
+    CmdCatNar()
+    {
+        expectArg("nar", &narPath);
+        expectArg("path", &path);
+    }
+
+    std::string name() override
+    {
+        return "cat-nar";
+    }
+
+    std::string description() override
+    {
+        return "print the contents of a file inside a NAR file";
+    }
+
+    void run(ref<Store> store) override
+    {
+        cat(makeNarAccessor(make_ref<std::string>(readFile(narPath))));
+    }
+};
+
+static RegisterCommand r1(make_ref<CmdCatStore>());
+static RegisterCommand r2(make_ref<CmdCatNar>());
diff --git a/src/nix/command.cc b/src/nix/command.cc
new file mode 100644
index 000000000000..9c80f43093c5
--- /dev/null
+++ b/src/nix/command.cc
@@ -0,0 +1,65 @@
+#include "command.hh"
+#include "store-api.hh"
+
+namespace nix {
+
+Commands * RegisterCommand::commands = 0;
+
+MultiCommand::MultiCommand(const Commands & _commands)
+    : commands(_commands)
+{
+    expectedArgs.push_back(ExpectedArg{"command", 1, [=](Strings ss) {
+        assert(!command);
+        auto i = commands.find(ss.front());
+        if (i == commands.end())
+            throw UsageError(format("‘%1%’ is not a recognised command") % ss.front());
+        command = i->second;
+    }});
+}
+
+void MultiCommand::printHelp(const string & programName, std::ostream & out)
+{
+    if (command) {
+        command->printHelp(programName + " " + command->name(), out);
+        return;
+    }
+
+    out << "Usage: " << programName << " <COMMAND> <FLAGS>... <ARGS>...\n";
+
+    out << "\n";
+    out << "Common flags:\n";
+    printFlags(out);
+
+    out << "\n";
+    out << "Available commands:\n";
+
+    Table2 table;
+    for (auto & command : commands)
+        table.push_back(std::make_pair(command.second->name(), command.second->description()));
+    printTable(out, table);
+
+    out << "\n";
+    out << "For full documentation, run ‘man " << programName << "’ or ‘man " << programName << "-<COMMAND>’.\n";
+}
+
+bool MultiCommand::processFlag(Strings::iterator & pos, Strings::iterator end)
+{
+    if (Args::processFlag(pos, end)) return true;
+    if (command && command->processFlag(pos, end)) return true;
+    return false;
+}
+
+bool MultiCommand::processArgs(const Strings & args, bool finish)
+{
+    if (command)
+        return command->processArgs(args, finish);
+    else
+        return Args::processArgs(args, finish);
+}
+
+void StoreCommand::run()
+{
+    run(openStore());
+}
+
+}
diff --git a/src/nix/command.hh b/src/nix/command.hh
new file mode 100644
index 000000000000..a84721ccfa8c
--- /dev/null
+++ b/src/nix/command.hh
@@ -0,0 +1,59 @@
+#pragma once
+
+#include "args.hh"
+
+namespace nix {
+
+/* A command is an argument parser that can be executed by calling its
+   run() method. */
+struct Command : virtual Args
+{
+    virtual std::string name() = 0;
+    virtual void prepare() { };
+    virtual void run() = 0;
+};
+
+class Store;
+
+/* A command that require a Nix store. */
+struct StoreCommand : virtual Command
+{
+    bool reserveSpace;
+    StoreCommand(bool reserveSpace = true)
+        : reserveSpace(reserveSpace) { };
+    void run() override;
+    virtual void run(ref<Store>) = 0;
+};
+
+typedef std::map<std::string, ref<Command>> Commands;
+
+/* An argument parser that supports multiple subcommands,
+   i.e. ‘<command> <subcommand>’. */
+struct MultiCommand : virtual Args
+{
+    Commands commands;
+
+    std::shared_ptr<Command> command;
+
+    MultiCommand(const Commands & commands);
+
+    void printHelp(const string & programName, std::ostream & out) override;
+
+    bool processFlag(Strings::iterator & pos, Strings::iterator end) override;
+
+    bool processArgs(const Strings & args, bool finish) override;
+};
+
+/* A helper class for registering commands globally. */
+struct RegisterCommand
+{
+    static Commands * commands;
+
+    RegisterCommand(ref<Command> command)
+    {
+        if (!commands) commands = new Commands;
+        commands->emplace(command->name(), command);
+    }
+};
+
+}
diff --git a/src/nix/hash.cc b/src/nix/hash.cc
new file mode 100644
index 000000000000..5dd891e8add3
--- /dev/null
+++ b/src/nix/hash.cc
@@ -0,0 +1,140 @@
+#include "command.hh"
+#include "hash.hh"
+#include "legacy.hh"
+#include "shared.hh"
+
+using namespace nix;
+
+struct CmdHash : Command
+{
+    enum Mode { mFile, mPath };
+    Mode mode;
+    bool base32 = false;
+    bool truncate = false;
+    HashType ht = htSHA512;
+    Strings paths;
+
+    CmdHash(Mode mode) : mode(mode)
+    {
+        mkFlag(0, "base32", "print hash in base-32", &base32);
+        mkFlag(0, "base16", "print hash in base-16", &base32, false);
+        mkHashTypeFlag("type", &ht);
+        expectArgs("paths", &paths);
+    }
+
+    std::string name() override
+    {
+        return mode == mFile ? "hash-file" : "hash-path";
+    }
+
+    std::string description() override
+    {
+        return mode == mFile
+            ? "print cryptographic hash of a regular file"
+            : "print cryptographic hash of the NAR serialisation of a path";
+    }
+
+    void run() override
+    {
+        for (auto path : paths) {
+            Hash h = mode == mFile ? hashFile(ht, path) : hashPath(ht, path).first;
+            if (truncate && h.hashSize > 20) h = compressHash(h, 20);
+            std::cout << format("%1%\n") %
+                (base32 ? printHash32(h) : printHash(h));
+        }
+    }
+};
+
+static RegisterCommand r1(make_ref<CmdHash>(CmdHash::mFile));
+static RegisterCommand r2(make_ref<CmdHash>(CmdHash::mPath));
+
+struct CmdToBase : Command
+{
+    bool toBase32;
+    HashType ht = htSHA512;
+    Strings args;
+
+    CmdToBase(bool toBase32) : toBase32(toBase32)
+    {
+        mkHashTypeFlag("type", &ht);
+        expectArgs("strings", &args);
+    }
+
+    std::string name() override
+    {
+        return toBase32 ? "to-base32" : "to-base16";
+    }
+
+    std::string description() override
+    {
+        return toBase32
+            ? "convert a hash to base-32 representation"
+            : "convert a hash to base-16 representation";
+    }
+
+    void run() override
+    {
+        for (auto s : args) {
+            Hash h = parseHash16or32(ht, s);
+            std::cout << format("%1%\n") %
+                (toBase32 ? printHash32(h) : printHash(h));
+        }
+    }
+};
+
+static RegisterCommand r3(make_ref<CmdToBase>(false));
+static RegisterCommand r4(make_ref<CmdToBase>(true));
+
+/* Legacy nix-hash command. */
+static int compatNixHash(int argc, char * * argv)
+{
+    HashType ht = htMD5;
+    bool flat = false;
+    bool base32 = false;
+    bool truncate = false;
+    enum { opHash, opTo32, opTo16 } op = opHash;
+    Strings ss;
+
+    parseCmdLine(argc, argv, [&](Strings::iterator & arg, const Strings::iterator & end) {
+        if (*arg == "--help")
+            showManPage("nix-hash");
+        else if (*arg == "--version")
+            printVersion("nix-hash");
+        else if (*arg == "--flat") flat = true;
+        else if (*arg == "--base32") base32 = true;
+        else if (*arg == "--truncate") truncate = true;
+        else if (*arg == "--type") {
+            string s = getArg(*arg, arg, end);
+            ht = parseHashType(s);
+            if (ht == htUnknown)
+                throw UsageError(format("unknown hash type ‘%1%’") % s);
+        }
+        else if (*arg == "--to-base16") op = opTo16;
+        else if (*arg == "--to-base32") op = opTo32;
+        else if (*arg != "" && arg->at(0) == '-')
+            return false;
+        else
+            ss.push_back(*arg);
+        return true;
+    });
+
+    if (op == opHash) {
+        CmdHash cmd(flat ? CmdHash::mFile : CmdHash::mPath);
+        cmd.ht = ht;
+        cmd.base32 = base32;
+        cmd.truncate = truncate;
+        cmd.paths = ss;
+        cmd.run();
+    }
+
+    else {
+        CmdToBase cmd(op == opTo32);
+        cmd.args = ss;
+        cmd.ht = ht;
+        cmd.run();
+    }
+
+    return 0;
+}
+
+static RegisterLegacyCommand s1("nix-hash", compatNixHash);
diff --git a/src/nix/installables.cc b/src/nix/installables.cc
new file mode 100644
index 000000000000..fb5a515825aa
--- /dev/null
+++ b/src/nix/installables.cc
@@ -0,0 +1,75 @@
+#include "attr-path.hh"
+#include "common-opts.hh"
+#include "derivations.hh"
+#include "eval-inline.hh"
+#include "eval.hh"
+#include "get-drvs.hh"
+#include "installables.hh"
+#include "store-api.hh"
+
+namespace nix {
+
+UserEnvElems MixInstallables::evalInstallables(ref<Store> store)
+{
+    UserEnvElems res;
+
+    for (auto & installable : installables) {
+
+        if (std::string(installable, 0, 1) == "/") {
+
+            if (isStorePath(installable)) {
+
+                if (isDerivation(installable)) {
+                    UserEnvElem elem;
+                    // FIXME: handle empty case, drop version
+                    elem.attrPath = {storePathToName(installable)};
+                    elem.isDrv = true;
+                    elem.drvPath = installable;
+                    res.push_back(elem);
+                }
+
+                else {
+                    UserEnvElem elem;
+                    // FIXME: handle empty case, drop version
+                    elem.attrPath = {storePathToName(installable)};
+                    elem.isDrv = false;
+                    elem.outPaths = {installable};
+                    res.push_back(elem);
+                }
+            }
+
+            else
+                throw UsageError(format("don't know what to do with ‘%1%’") % installable);
+        }
+
+        else {
+
+            EvalState state({}, store);
+
+            Expr * e = state.parseExprFromFile(resolveExprPath(lookupFileArg(state, file)));
+
+            Value vRoot;
+            state.eval(e, vRoot);
+
+            std::map<string, string> autoArgs_;
+            Bindings & autoArgs(*evalAutoArgs(state, autoArgs_));
+
+            Value & v(*findAlongAttrPath(state, installable, autoArgs, vRoot));
+            state.forceValue(v);
+
+            DrvInfos drvs;
+            getDerivations(state, v, "", autoArgs, drvs, false);
+
+            for (auto & i : drvs) {
+                UserEnvElem elem;
+                elem.isDrv = true;
+                elem.drvPath = i.queryDrvPath();
+                res.push_back(elem);
+            }
+        }
+    }
+
+    return res;
+}
+
+}
diff --git a/src/nix/installables.hh b/src/nix/installables.hh
new file mode 100644
index 000000000000..5eb897d46148
--- /dev/null
+++ b/src/nix/installables.hh
@@ -0,0 +1,38 @@
+#pragma once
+
+#include "args.hh"
+
+namespace nix {
+
+struct UserEnvElem
+{
+    Strings attrPath;
+
+    // FIXME: should use boost::variant or so.
+    bool isDrv;
+
+    // Derivation case:
+    Path drvPath;
+    StringSet outputNames;
+
+    // Non-derivation case:
+    PathSet outPaths;
+};
+
+typedef std::vector<UserEnvElem> UserEnvElems;
+
+struct MixInstallables : virtual Args
+{
+    Strings installables;
+    Path file = "<nixpkgs>";
+
+    MixInstallables()
+    {
+        mkFlag('f', "file", "file", "evaluate FILE rather than the default", &file);
+        expectArgs("installables", &installables);
+    }
+
+    UserEnvElems evalInstallables(ref<Store> store);
+};
+
+}
diff --git a/src/nix/legacy.cc b/src/nix/legacy.cc
new file mode 100644
index 000000000000..6df09ee37a5e
--- /dev/null
+++ b/src/nix/legacy.cc
@@ -0,0 +1,7 @@
+#include "legacy.hh"
+
+namespace nix {
+
+RegisterLegacyCommand::Commands * RegisterLegacyCommand::commands = 0;
+
+}
diff --git a/src/nix/legacy.hh b/src/nix/legacy.hh
new file mode 100644
index 000000000000..b67b70eb5c85
--- /dev/null
+++ b/src/nix/legacy.hh
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <functional>
+#include <map>
+
+namespace nix {
+
+typedef std::function<void(int, char * *)> MainFunction;
+
+struct RegisterLegacyCommand
+{
+    typedef std::map<std::string, MainFunction> Commands;
+    static Commands * commands;
+
+    RegisterLegacyCommand(const std::string & name, MainFunction fun)
+    {
+        if (!commands) commands = new Commands;
+        (*commands)[name] = fun;
+    }
+};
+
+}
diff --git a/src/nix/local.mk b/src/nix/local.mk
new file mode 100644
index 000000000000..f6e7073b6e7d
--- /dev/null
+++ b/src/nix/local.mk
@@ -0,0 +1,9 @@
+programs += nix
+
+nix_DIR := $(d)
+
+nix_SOURCES := $(wildcard $(d)/*.cc)
+
+nix_LIBS = libexpr libmain libstore libutil libformat
+
+$(eval $(call install-symlink, nix, $(bindir)/nix-hash))
diff --git a/src/nix/ls.cc b/src/nix/ls.cc
new file mode 100644
index 000000000000..3476dfb05287
--- /dev/null
+++ b/src/nix/ls.cc
@@ -0,0 +1,123 @@
+#include "command.hh"
+#include "store-api.hh"
+#include "fs-accessor.hh"
+#include "nar-accessor.hh"
+
+using namespace nix;
+
+struct MixLs : virtual Args
+{
+    std::string path;
+
+    bool recursive = false;
+    bool verbose = false;
+    bool showDirectory = false;
+
+    MixLs()
+    {
+        mkFlag('R', "recursive", "list subdirectories recursively", &recursive);
+        mkFlag('l', "long", "show more file information", &verbose);
+        mkFlag('d', "directory", "show directories rather than their contents", &showDirectory);
+    }
+
+    void list(ref<FSAccessor> accessor)
+    {
+        std::function<void(const FSAccessor::Stat &, const Path &, const std::string &, bool)> doPath;
+
+        auto showFile = [&](const Path & curPath, const std::string & relPath) {
+            if (verbose) {
+                auto st = accessor->stat(curPath);
+                std::string tp =
+                    st.type == FSAccessor::Type::tRegular ?
+                        (st.isExecutable ? "-r-xr-xr-x" : "-r--r--r--") :
+                    st.type == FSAccessor::Type::tSymlink ? "lrwxrwxrwx" :
+                    "dr-xr-xr-x";
+                std::cout <<
+                    (format("%s %20d %s") % tp % st.fileSize % relPath);
+                if (st.type == FSAccessor::Type::tSymlink)
+                    std::cout << " -> " << accessor->readLink(curPath)
+                    ;
+                std::cout << "\n";
+                if (recursive && st.type == FSAccessor::Type::tDirectory)
+                    doPath(st, curPath, relPath, false);
+            } else {
+                std::cout << relPath << "\n";
+                if (recursive) {
+                    auto st = accessor->stat(curPath);
+                    if (st.type == FSAccessor::Type::tDirectory)
+                        doPath(st, curPath, relPath, false);
+                }
+            }
+        };
+
+        doPath = [&](const FSAccessor::Stat & st , const Path & curPath,
+            const std::string & relPath, bool showDirectory)
+        {
+            if (st.type == FSAccessor::Type::tDirectory && !showDirectory) {
+                auto names = accessor->readDirectory(curPath);
+                for (auto & name : names)
+                    showFile(curPath + "/" + name, relPath + "/" + name);
+            } else
+                showFile(curPath, relPath);
+        };
+
+        auto st = accessor->stat(path);
+        if (st.type == FSAccessor::Type::tMissing)
+            throw Error(format("path ‘%1%’ does not exist") % path);
+        doPath(st, path,
+            st.type == FSAccessor::Type::tDirectory ? "." : baseNameOf(path),
+            showDirectory);
+    }
+};
+
+struct CmdLsStore : StoreCommand, MixLs
+{
+    CmdLsStore()
+    {
+        expectArg("path", &path);
+    }
+
+    std::string name() override
+    {
+        return "ls-store";
+    }
+
+    std::string description() override
+    {
+        return "show information about a store path";
+    }
+
+    void run(ref<Store> store) override
+    {
+        list(store->getFSAccessor());
+    }
+};
+
+struct CmdLsNar : Command, MixLs
+{
+    Path narPath;
+
+    CmdLsNar()
+    {
+        expectArg("nar", &narPath);
+        expectArg("path", &path);
+    }
+
+    std::string name() override
+    {
+        return "ls-nar";
+    }
+
+    std::string description() override
+    {
+        return "show information about the contents of a NAR file";
+    }
+
+    void run() override
+    {
+        list(makeNarAccessor(make_ref<std::string>(readFile(narPath))));
+    }
+};
+
+static RegisterCommand r1(make_ref<CmdLsStore>());
+static RegisterCommand r2(make_ref<CmdLsNar>());
diff --git a/src/nix/main.cc b/src/nix/main.cc
new file mode 100644
index 000000000000..20d3ea5c2555
--- /dev/null
+++ b/src/nix/main.cc
@@ -0,0 +1,55 @@
+#include <algorithm>
+
+#include "command.hh"
+#include "common-args.hh"
+#include "eval.hh"
+#include "globals.hh"
+#include "legacy.hh"
+#include "shared.hh"
+#include "store-api.hh"
+
+namespace nix {
+
+struct NixArgs : virtual MultiCommand, virtual MixCommonArgs
+{
+    NixArgs() : MultiCommand(*RegisterCommand::commands), MixCommonArgs("nix")
+    {
+        mkFlag('h', "help", "show usage information", [=]() {
+            printHelp(programName, std::cout);
+            throw Exit();
+        });
+
+        mkFlag(0, "version", "show version information", std::bind(printVersion, programName));
+    }
+};
+
+void mainWrapped(int argc, char * * argv)
+{
+    initNix();
+    initGC();
+
+    string programName = baseNameOf(argv[0]);
+
+    {
+        auto legacy = (*RegisterLegacyCommand::commands)[programName];
+        if (legacy) return legacy(argc, argv);
+    }
+
+    NixArgs args;
+
+    args.parseCmdline(argvToStrings(argc, argv));
+
+    assert(args.command);
+
+    args.command->prepare();
+    args.command->run();
+}
+
+}
+
+int main(int argc, char * * argv)
+{
+    return nix::handleExceptions(argv[0], [&]() {
+        nix::mainWrapped(argc, argv);
+    });
+}