diff --git a/CHANGES.md b/CHANGES.md
index e79a170839b..98a9cd0ca84 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -14,6 +14,9 @@ unreleased
 - Add support for library variants and default implementations. (#1900,
   @TheLortex)
 
+- Add experimental `$ dune init` command. This command is used to create or
+  update project boilerplate. (#1448, fixes #159, @shonfeder)
+
 1.8.2 (10/03/2019)
 ------------------
 
diff --git a/bin/common.mli b/bin/common.mli
index 7d96f6c17fd..742b913224c 100644
--- a/bin/common.mli
+++ b/bin/common.mli
@@ -27,10 +27,19 @@ type t =
 
 val prefix_target : t -> string -> string
 
+(** [set_common common ~targets] is [set_dirs common] followed by
+    [set_common_other common ~targets]. In general, [set_common] executes
+    sequence of side-effecting actions to initialize Dune's working
+    environment based on the options determined in a [Common.t] record *)
 val set_common : t -> targets:string list -> unit
 
+(** [set_common_other common ~targets] sets all stateful values dictated by
+    [common], except those accounted for by [set_dirs]. [targets] are
+    used to obtain external library dependency hints, if needed. *)
 val set_common_other : t -> targets:string list -> unit
 
+(** [set_dirs common] sets the workspace root and build directories, and makes
+ the root the current working directory *)
 val set_dirs : t -> unit
 
 val help_secs
diff --git a/bin/init.ml b/bin/init.ml
new file mode 100644
index 00000000000..397743e02d6
--- /dev/null
+++ b/bin/init.ml
@@ -0,0 +1,115 @@
+open Stdune
+open Import
+
+open Dune.Dune_init
+
+(* TODO(shonfeder): Remove when nested subcommands are available *)
+let validate_component_options kind ~unsupported_options =
+  let report_invalid_option = function
+    | _, false -> ()  (* The option wasn't supplied *)
+    | option_name, true ->
+      die "The %s component does not support the %s option"
+        (Kind.to_string kind) option_name
+  in
+  List.iter ~f:report_invalid_option unsupported_options
+
+let doc = "Initialize dune components"
+let man =
+  [ `S "DESCRIPTION"
+  ; `P {|$(b,dune init {lib,exe,test} NAME [PATH]) initialize a new dune
+         component of the specified kind, named $(b,NAME), with fields
+         determined by the supplied options.|}
+  ; `P {|If the optional $(b,PATH) is provided, the project will be created
+         there. Otherwise, it is created in the current working directory.|}
+  ; `P {|The command can be used to add stanzas to existing dune files as
+         well as for creating new dune files and basic component templates.|}
+  ; `S "EXAMPLES"
+  ; `Pre {|
+Define an executable component named 'myexe' in a dune file in the
+current directory:
+
+          dune init exe myexe
+
+Define a library component named 'mylib' in a dune file in the ./src
+directory depending on the core and cmdliner libraries, the ppx_let
+and ppx_inline_test preprocessors, and declared as using inline tests:
+
+          dune init lib mylib src --libs core,cmdliner --ppx ppx_let,ppx_inline_test --inline-tests
+
+Define a library component named mytest in a dune file in the ./test
+directory that depends on mylib:
+
+        dune init test myexe test --libs mylib|}
+  ]
+
+let info = Term.info "init" ~doc ~man
+
+let term =
+  let+ common_term = Common.term
+  and+ kind =
+    (* TODO(shonfeder): Replace with nested subcommand once we have support for that *)
+    Arg.(required & pos 0 (some (enum Kind.commands)) None & info [] ~docv:"INIT_KIND")
+  and+ name =
+    Arg.(required & pos 1 (some string) None & info [] ~docv:"NAME")
+  and+ path =
+    Arg.(value & pos 2 (some string) None & info [] ~docv:"PATH" )
+  and+ libraries =
+    Arg.(value
+         & opt (list string) []
+         & info ["libs"]
+             ~docv:"LIBRARIES"
+             ~doc:"Libraries on which the component depends")
+  and+ pps =
+    Arg.(value
+         & opt (list string) []
+         & info ["ppx"]
+             ~docv:"PREPROCESSORS"
+             ~doc:"ppx preprocessors used by the component")
+  and+ public =
+    (* TODO(shonfeder): Move to subcommands {lib, exe} once implemented *)
+    Arg.(value
+         & opt ~vopt:(Some "") (some string) None
+         & info ["public"]
+             ~docv:"PUBLIC_NAME"
+             ~doc:"If called with an argument, make the component public \
+                   under the given PUBLIC_NAME. If supplied without an \
+                   argument, use NAME.")
+  and+ inline_tests =
+    (* TODO Move to subcommand lib once implemented *)
+    Arg.(value
+         & flag
+         & info ["inline-tests"]
+             ~docv:"USE_INLINE_TESTS"
+             ~doc:"Whether to use inline tests. \
+                   Only applicable for lib components.")
+  in
+
+  validate_component_name name;
+
+  Common.set_common common_term ~targets:[];
+  let open Component in
+  let context = Init_context.make path in
+  let common : Options.common = { name; libraries; pps } in
+  let given_public = Option.is_some public in
+  begin match kind with
+  | Kind.Library ->
+    init @@ Library { context; common; options = {public; inline_tests} }
+  | Kind.Executable ->
+    let unsupported_options =
+      ["inline-tests", inline_tests]
+    in
+    validate_component_options kind ~unsupported_options;
+    init @@ Executable { context; common; options = {public} }
+  | Kind.Test ->
+    let unsupported_options =
+      [ "public", given_public
+      ; "inline-tests", inline_tests]
+    in
+    validate_component_options kind ~unsupported_options;
+    init @@ Test { context; common; options = () }
+  end;
+
+  print_completion kind name
+
+let command = term, info
+
diff --git a/bin/init.mli b/bin/init.mli
new file mode 100644
index 00000000000..6d988967f3a
--- /dev/null
+++ b/bin/init.mli
@@ -0,0 +1 @@
+val command : unit Cmdliner.Term.t * Cmdliner.Term.info
diff --git a/bin/main.ml b/bin/main.ml
index 8abea47d3b2..646e1b667cb 100644
--- a/bin/main.ml
+++ b/bin/main.ml
@@ -136,6 +136,7 @@ let all =
   ; Subst.command
   ; Print_rules.command
   ; Utop.command
+  ; Init.command
   ; promote
   ; Printenv.command
   ; Help.command
diff --git a/doc/dune.inc b/doc/dune.inc
index f461112c630..f336d997577 100644
--- a/doc/dune.inc
+++ b/doc/dune.inc
@@ -62,6 +62,15 @@
  (package dune)
  (files   dune-help.1))
 
+(rule
+ (with-stdout-to dune-init.1
+  (run dune init --help=groff)))
+
+(install
+ (section man)
+ (package dune)
+ (files   dune-init.1))
+
 (rule
  (with-stdout-to dune-install.1
   (run dune install --help=groff)))
diff --git a/doc/update-jbuild.sh b/doc/update-jbuild.sh
index a1b2cee34d3..2db491c3a5c 100755
--- a/doc/update-jbuild.sh
+++ b/doc/update-jbuild.sh
@@ -5,7 +5,7 @@
 set -e -o pipefail
 
 CMDS=$(dune --help=plain | \
-           sed -n '/COMMANDS/,/OPTIONS/p' | sed -En 's/^       ([a-z-]+)/\1/p')
+           sed -n '/COMMANDS/,/OPTIONS/p' | sed -En 's/^       ([a-z-]+) ?.*/\1/p')
 
 for cmd in $CMDS; do
     cat <<EOF
diff --git a/doc/usage.rst b/doc/usage.rst
index f7cd8f55e12..6236e9a274e 100644
--- a/doc/usage.rst
+++ b/doc/usage.rst
@@ -4,6 +4,58 @@ Usage
 
 This section describe usage of dune from the shell.
 
+.. _initializing_components:
+
+Initializing components
+=======================
+
+NOTE: The ``dune init`` command is still under development and subject to
+change.
+
+Dune's ``init`` subcommand provides limited support for generating dune file
+stanzas and folder structures to define components. ``dune init`` can be used to
+quickly add new libraries, tests, or executables without having to manually edit
+a dune file, or it can be composed to programmatically generate parts of a
+multi-component project.
+
+For example, to add a new executable to a ``dune`` file in the current directory
+(creating the file if necessary), you can run
+
+.. code:: bash
+
+    $ dune init exe myexe --libs base,containers,notty --ppx ppx_deriving
+
+This will add the following stanza to the ``dune`` file:
+
+.. code:: scheme
+
+    (executable
+     (name main)
+     (libraries base containers notty)
+     (preprocess
+      (pps ppx_deriving)))
+
+Or, to create a new directory ``src``, initialized as a library, you can run:
+
+.. code:: bash
+
+    $ dune init lib mylib src --libs core --inline-tests --public
+
+This will ensure the file ``./src/dune`` contains the following stanza (creating
+the file and directory, if needed):
+
+.. code:: scheme
+
+    (library
+     (public_name mylib)
+     (inline_tests)
+     (name mylib)
+     (libraries core)
+     (preprocess
+      (pps ppx_inline_tests)))
+
+Consult the manual page ``dune init --help`` for more details.
+
 .. _finding-root:
 
 Finding the root
@@ -505,7 +557,7 @@ must be prefixed by the shortest one.
 Watermarking
 ============
 
-One of the feature dune-release provides is watermarking; it replaces
+One of the features dune-release provides is watermarking; it replaces
 various strings of the form ``%%ID%%`` in all files of your project
 before creating a release tarball or when the package is pinned by the
 user using opam.
diff --git a/src/dune_file.mli b/src/dune_file.mli
index a936214d5af..86df0d8c41e 100644
--- a/src/dune_file.mli
+++ b/src/dune_file.mli
@@ -427,6 +427,15 @@ module Stanzas : sig
 
   type syntax = OCaml | Plain
 
+  (** [parse ~file ~kind project stanza_exprs] is a list of [Stanza.t]s derived
+      from decoding the [stanza_exprs] from [Dune_lang.Ast.t]s to [Stanza.t]s
+      and combining those with the stanzas parsed from the supplied dune [file].
+
+      The stanzas are parsed in the context of the dune [project].
+
+      The syntax [kind] determines whether the expected syntax is either the
+      depreciated jbuilder syntax or the version of dune syntax specified in the
+      [project]. *)
   val parse
     :  file:Path.t
     -> kind:Dune_lang.Syntax.t
diff --git a/src/dune_init.ml b/src/dune_init.ml
new file mode 100644
index 00000000000..8b803abb2cc
--- /dev/null
+++ b/src/dune_init.ml
@@ -0,0 +1,377 @@
+open! Stdune
+open! Import
+
+(** Because the dune_init utility deals with the addition of stanzas and
+    fields to dune projects and files, we need to inspect and manipulate the
+    concrete syntax tree (CST) a good deal. *)
+module Cst = Dune_lang.Cst
+
+module Kind = struct
+  type t =
+    | Executable
+    | Library
+    | Test
+
+  let to_string = function
+    | Executable -> "executable"
+    | Library -> "library"
+    | Test -> "test"
+
+  let pp ppf t = Format.pp_print_string ppf (to_string t)
+
+  let commands =
+    [ "exe", Executable
+    ; "lib", Library
+    ; "test", Test
+    ]
+end
+
+(** Abstractions around the kinds of files handled during initialization *)
+module File = struct
+
+  type dune =
+    { path: Path.t
+    ; name: string
+    ; content: Cst.t list
+    }
+
+  type text =
+    { path: Path.t
+    ; name: string
+    ; content: string
+    }
+
+  type t =
+    | Dune of dune
+    | Text of text
+
+  let make_text path name content =
+    Text {path; name; content}
+
+  let full_path = function
+    | Dune {path; name; _} | Text {path; name; _} ->
+       Path.relative path name
+
+  (** Inspection and manipulation of stanzas in a file *)
+  module Stanza = struct
+
+    (** Defines uniqueness criteria for stanzas *)
+    module Signature = struct
+
+      (** The uniquely identifying fields of a stanza *)
+      type t =
+        { kind: string
+        ; name: string option
+        ; public_name: string option
+        }
+
+      (* TODO(shonfeder): replace with stanza merging *)
+      (* TODO(shonfeder): replace with a function Cst.t -> Dune_file.Stanza.t *)
+      let of_cst stanza : t option =
+        let open Dune_lang in
+        let open Option.O in
+        let to_atom = function | Atom a -> Some a | _ -> None in
+        let is_field name = function
+          | List (field_name :: _) ->
+            Option.value ~default:false
+              (to_atom field_name >>| Atom.equal (Atom.of_string name))
+          | _ -> false
+        in
+        Cst.to_sexp stanza >>= function
+        | List (component_kind :: fields) ->
+          let find_field_value field_name fields =
+            List.find ~f:(is_field field_name) fields >>= function
+            | List [_; value] -> Some (to_string ~syntax:Dune value)
+            | _ -> None
+          in
+          let kind = to_string ~syntax:Dune component_kind in
+          let name = find_field_value "name" fields in
+          let public_name = find_field_value "public_name" fields in
+          Some {kind; name; public_name}
+        | _ -> None
+
+      let equal a b =
+        (* Like Option.equal but doesn't treat None's as equal *)
+        let strict_equal x y =
+          match x, y with
+          | Some x, Some y -> String.equal x y
+          | _, _ -> false
+        in
+        String.equal a.kind b.kind
+        && strict_equal a.name b.name
+        || strict_equal a.public_name b.public_name
+    end
+
+    let pp ppf s =
+      Option.iter (Cst.to_sexp s) ~f:(Dune_lang.pp Dune ppf)
+
+    (* TODO(shonfeder): replace with stanza merging *)
+    let find_conflicting new_stanzas existing_stanzas =
+      let stanzas_conflict a b =
+        (let open Option.O in
+         let* a = Signature.of_cst a in
+         let+ b = Signature.of_cst b in
+         Signature.equal a b)
+        |> Option.value ~default:false
+      in
+      let conflicting_stanza stanza =
+        match List.find ~f:(stanzas_conflict stanza) existing_stanzas with
+        | Some conflict -> Some (stanza, conflict)
+        | None -> None
+      in
+      List.find_map ~f:conflicting_stanza new_stanzas
+
+    let add stanzas = function
+      | Text f -> Text f (* Adding a stanza to a text file isn't meaningful *)
+      | Dune f ->
+        match find_conflicting stanzas f.content with
+        | None -> Dune {f with content = f.content @ stanzas}
+        | Some (a, b) ->
+          die "Updating existing stanzas is not yet supported.@\n\
+               A preexisting dune stanza conflicts with a generated stanza:\
+               @\n@\nGenerated stanza:@.%a@.@.Pre-existing stanza:@.%a"
+            pp a pp b
+  end (* Stanza *)
+
+  let create_dir path =
+    try Path.mkdir_p path with
+    | Unix.Unix_error (EACCES, _, _) ->
+      die "A project directory cannot be created or accessed: \
+           Lacking permissions needed to create directory %a"
+        Path.pp path
+
+  let load_dune_file ~path =
+    let name = "dune" in
+    let full_path = Path.relative path name in
+    let content =
+      if not (Path.exists full_path) then
+        []
+      else
+        match Format_dune_lang.parse_file (Some full_path) with
+        | Format_dune_lang.Sexps content -> content
+        | Format_dune_lang.OCaml_syntax _ ->
+          die "Cannot load dune file %a because it uses OCaml syntax"
+            Path.pp full_path
+    in
+    Dune {path; name; content}
+
+  let write_dune_file (dune_file : dune) =
+    let path = Path.relative dune_file.path dune_file.name in
+    Format_dune_lang.write_file ~path dune_file.content
+
+  let write f =
+    let path = full_path f in
+    match f with
+    | Dune f -> Ok (write_dune_file f)
+    | Text f ->
+      if Path.exists path then
+        Error path
+      else
+        Ok (Io.write_file ~binary:false path f.content)
+end
+
+(** The context in which the initialization is executed *)
+module Init_context = struct
+  type t =
+    { dir : Path.t
+    ; project : Dune_project.t
+    }
+
+  let make path =
+    let project =
+      match Dune_project.load ~dir:Path.root ~files:String.Set.empty with
+      | Some p -> p
+      | None   -> Lazy.force Dune_project.anonymous
+    in
+    let dir =
+      match path with
+      | None -> Path.root
+      | Some p -> Path.of_string p
+    in
+    File.create_dir dir;
+    { dir; project }
+end
+
+module Component = struct
+
+  module Options = struct
+    type common =
+      { name : string
+      ; libraries : string list
+      ; pps : string list
+      }
+
+    type executable =
+      { public: string option
+      }
+
+    type library =
+      { public: string option
+      ; inline_tests: bool
+      }
+
+    (* NOTE: no options supported yet *)
+    type test = ()
+
+    type 'options t =
+      { context : Init_context.t
+      ; common : common
+      ; options : 'options
+      }
+  end
+
+  type 'options t =
+    | Executable : Options.executable Options.t -> Options.executable t
+    | Library : Options.library Options.t -> Options.library t
+    | Test : Options.test Options.t -> Options.test t
+
+  type target =
+    { dir : Path.t
+    ; files : File.t list
+    }
+
+  (** Creates Dune language CST stanzas describing components *)
+  module Stanza_cst = struct
+    open Dune_lang
+
+    module Field = struct
+      let atoms = List.map ~f:atom
+      let public_name name = List [atom "public_name"; atom name]
+      let name name = List [atom "name"; atom name]
+      let inline_tests = List [atom "inline_tests"]
+      let libraries libs = List (atom "libraries" :: atoms libs)
+      let pps pps = List [atom "preprocess"; List (atom "pps" :: atoms pps)]
+
+      let optional_field ~f = function
+        | [] -> []
+        | args -> [f args]
+
+      let common (options : Options.common) =
+        let optional_fields =
+          optional_field ~f:libraries options.libraries
+          @ optional_field ~f:pps options.pps
+        in
+        name options.name :: optional_fields
+    end
+
+    let make kind common_options fields  =
+      (* Form the AST *)
+      List (atom kind
+            :: fields
+            @ Field.common common_options)
+      (* Convert to a CST *)
+      |> Dune_lang.add_loc ~loc:Loc.none
+      |> Cst.concrete
+      (* Package as a list CSTs *)
+      |> List.singleton
+
+    let add_to_list_set elem set =
+      if List.mem elem ~set then set else elem :: set
+
+    let public_name_field ~default = function
+      | None -> []
+      | Some "" -> [Field.public_name default]
+      | Some n  -> [Field.public_name n]
+
+    let executable (common : Options.common) (options : Options.executable) =
+      let public_name =
+        public_name_field ~default:common.name options.public
+      in
+      make "executable" {common with name = "main"} public_name
+
+    let library (common : Options.common) (options: Options.library) =
+      let (common, inline_tests) =
+        if not options.inline_tests then
+          (common, [])
+        else
+          let pps =
+            add_to_list_set "ppx_inline_tests" common.pps
+          in
+          ({common with pps}, [Field.inline_tests])
+      in
+      let public_name =
+        public_name_field ~default:common.name options.public
+      in
+      make "library" common (public_name @ inline_tests)
+
+    let test common ((): Options.test) =
+      make "test" common []
+  end
+
+  (* TODO Support for merging in changes to an existing stanza *)
+  let add_stanza_to_dune_file ~dir stanza =
+    File.load_dune_file ~path:dir
+    |> File.Stanza.add stanza
+
+  let bin ({context; common; options} : Options.executable Options.t) =
+    let dir = context.dir in
+    let bin_dune =
+      Stanza_cst.executable common options
+      |> add_stanza_to_dune_file ~dir
+    in
+    let bin_ml =
+      let name = "main.ml" in
+      let content = sprintf "let () = print_endline \"Hello, World!\"\n" in
+      File.make_text dir name content
+    in
+    let files = [bin_dune; bin_ml] in
+    {dir; files}
+
+  let src ({context; common; options} : Options.library Options.t) =
+    let dir = context.dir in
+    let lib_dune =
+      Stanza_cst.library common options
+      |> add_stanza_to_dune_file ~dir
+    in
+    let files = [lib_dune] in
+    {dir; files}
+
+  let test ({context; common; options}: Options.test Options.t) =
+    (* Marking the current absence of test-specific options *)
+    let dir = context.dir in
+    let test_dune =
+      Stanza_cst.test common options
+      |> add_stanza_to_dune_file ~dir
+    in
+    let test_ml =
+      let name = sprintf "%s.ml" common.name in
+      let content = "" in
+      File.make_text dir name content
+    in
+    let files = [test_dune; test_ml] in
+    {dir; files}
+
+  let report_uncreated_file = function
+    | Ok _ -> ()
+    | Error path ->
+      Errors.kerrf ~f:print_to_console
+         "@{<warning>Warning@}: file @{<kwd>%a@} was not created \
+          because it already exists\n"
+         Path.pp path
+
+  let create target =
+    File.create_dir target.dir;
+    List.map ~f:File.write target.files
+
+  let init (type options) (t : options t) =
+    let target =
+      match t with
+      | Executable params -> bin params
+      | Library params    -> src params
+      | Test params       -> test params
+    in
+    create target
+    |> List.iter ~f:report_uncreated_file
+end
+
+let validate_component_name name =
+  match Lib_name.Local.of_string name with
+  | Ok _ -> ()
+  | _    ->
+    die "A component named '%s' cannot be created because it is an %s"
+      name Lib_name.Local.invalid_message
+
+let print_completion kind name =
+  Errors.kerrf ~f:print_to_console
+    "@{<ok>Success@}: initialized %a component named @{<kwd>%s@}\n"
+    Kind.pp kind name
diff --git a/src/dune_init.mli b/src/dune_init.mli
new file mode 100644
index 00000000000..33c2c63d285
--- /dev/null
+++ b/src/dune_init.mli
@@ -0,0 +1,68 @@
+(** Initialize dune components *)
+open! Stdune
+
+(** Supported kinds of components for initialization *)
+module Kind : sig
+  type t =
+    | Executable
+    | Library
+    | Test
+
+  val to_string : t -> string
+  (* val kind_strings : string list *)
+  val commands : (string * t) list
+end
+
+(** The context in which the initialization is executed *)
+module Init_context : sig
+  type t =
+    { dir : Path.t
+    ; project : Dune_project.t
+    }
+
+  val make : string option -> t
+end
+
+(** A [Component.t] is a set of files that can be built or included as part of a
+    build. *)
+module Component : sig
+
+  (** Options determining the details of a generated component *)
+  module Options : sig
+    type common =
+      { name: string
+      ; libraries: string list
+      ; pps: string list
+      }
+
+    type executable =
+      { public: string option
+      }
+
+    type library =
+      { public: string option
+      ; inline_tests: bool
+      }
+
+    (** NOTE: no options supported yet *)
+    type test = ()
+
+    type 'a t =
+      { context : Init_context.t
+      ; common : common
+      ; options : 'a
+      }
+  end
+
+  type 'options t =
+    | Executable : Options.executable Options.t -> Options.executable t
+    | Library : Options.library Options.t -> Options.library t
+    | Test : Options.test Options.t -> Options.test t
+
+  (** Create or update the component specified by the ['options t],
+      where ['options] is *)
+  val init : 'options t -> unit
+end
+
+val validate_component_name : string -> unit
+val print_completion : Kind.t -> string -> unit
diff --git a/src/dune_lang/atom.ml b/src/dune_lang/atom.ml
index d214e11f102..d0c5b4bf373 100644
--- a/src/dune_lang/atom.ml
+++ b/src/dune_lang/atom.ml
@@ -2,6 +2,8 @@ open Stdune
 
 type t = A of string [@@unboxed]
 
+let equal (A a) (A b) = String.equal a b
+
 let is_valid_dune =
   let rec loop s i len =
     i = len ||
diff --git a/src/dune_lang/atom.mli b/src/dune_lang/atom.mli
index 7228737a71f..68fd2d922b7 100644
--- a/src/dune_lang/atom.mli
+++ b/src/dune_lang/atom.mli
@@ -2,6 +2,8 @@ open Stdune
 
 type t = private A of string [@@unboxed]
 
+val equal : t -> t -> bool
+
 val is_valid_dune : string -> bool
 val is_valid : t -> Syntax.t -> bool
 
diff --git a/src/dune_lang/dune_lang.ml b/src/dune_lang/dune_lang.ml
index b6b6388850b..4851b7b0c18 100644
--- a/src/dune_lang/dune_lang.ml
+++ b/src/dune_lang/dune_lang.ml
@@ -215,6 +215,9 @@ module Cst = struct
     | Template t -> Template t
     | List (loc, l) -> List (loc, List.map ~f:concrete l)
 
+  let to_sexp c =
+    abstract c |> Option.map ~f:Ast.remove_locs
+
   let extract_comments =
     let rec loop acc = function
       | Atom _ | Quoted_string _ | Template _ -> acc
diff --git a/src/dune_lang/dune_lang.mli b/src/dune_lang/dune_lang.mli
index f5e3b06a98b..5b80b1deb9c 100644
--- a/src/dune_lang/dune_lang.mli
+++ b/src/dune_lang/dune_lang.mli
@@ -7,6 +7,7 @@ module Atom : sig
   type t = private A of string [@@unboxed]
 
   val is_valid : t -> Syntax.t -> bool
+  val equal : t -> t -> bool
 
   val of_string : string -> t
   val to_string : t -> string
@@ -111,6 +112,7 @@ val add_loc : t -> loc:Loc.t -> Ast.t
 
 (** Concrete syntax tree *)
 module Cst : sig
+  type sexp = t
   module Comment : sig
     type t =
       | Lines of string list
@@ -150,9 +152,11 @@ module Cst : sig
 
   val concrete : Ast.t -> t
 
+  val to_sexp : t -> sexp option
+
   (** Return all the comments contained in a concrete syntax tree *)
   val extract_comments : t list -> (Loc.t * Comment.t) list
-end
+end with type sexp := t
 
 (** Insert comments in a concrete syntax tree. Comments are inserted
     based on their location. *)
diff --git a/src/dune_project.ml b/src/dune_project.ml
index e4df98bb0c9..25db6804b1e 100644
--- a/src/dune_project.ml
+++ b/src/dune_project.ml
@@ -222,6 +222,10 @@ module Project_file_edit = struct
   let notify_user s =
     kerrf ~f:print_to_console "@{<warning>Info@}: %s\n" s
 
+  let lang_stanza () =
+    let ver = (Lang.get_exn "dune").version in
+    sprintf "(lang dune %s)" (Syntax.Version.to_string ver)
+
   let ensure_exists t =
     if t.exists then
       Already_exist
@@ -262,6 +266,8 @@ module Project_file_edit = struct
     what
 end
 
+let lang_stanza = Project_file_edit.lang_stanza
+
 let ensure_project_file_exists t =
   Project_file_edit.ensure_exists t.project_file
 
diff --git a/src/dune_project.mli b/src/dune_project.mli
index 3472e4c8872..f211d2fbeda 100644
--- a/src/dune_project.mli
+++ b/src/dune_project.mli
@@ -106,6 +106,9 @@ val anonymous : t Lazy.t
 
 type created_or_already_exist = Created | Already_exist
 
+(** Generate an appropriate project [lang] stanza *)
+val lang_stanza : unit -> string
+
 (** Check that the dune-project file exists and create it otherwise. *)
 val ensure_project_file_exists : t -> created_or_already_exist
 
diff --git a/src/format_dune_lang.ml b/src/format_dune_lang.ml
index f461aaf9267..ef0071e3335 100644
--- a/src/format_dune_lang.ml
+++ b/src/format_dune_lang.ml
@@ -119,6 +119,13 @@ let pp_top_sexp fmt sexp =
 let pp_top_sexps =
   Fmt.list ~pp_sep:Fmt.nl pp_top_sexp
 
+let write_file ~path sexps =
+  let f oc =
+    let fmt = (Format.formatter_of_out_channel oc) in
+    Format.fprintf fmt "%a%!" pp_top_sexps sexps
+  in
+  Io.with_file_out ~binary:true path ~f
+
 let format_file ~input =
   match parse_file input with
   | exception Dune_lang.Parse_error e ->
diff --git a/src/format_dune_lang.mli b/src/format_dune_lang.mli
index 63fbb0958d3..2a9185a4324 100644
--- a/src/format_dune_lang.mli
+++ b/src/format_dune_lang.mli
@@ -1,5 +1,15 @@
 open Import
 
+type dune_file =
+  | OCaml_syntax of Loc.t
+  | Sexps of Dune_lang.Cst.t list
+
+(** Read a file into its concrete syntax *)
+val parse_file : Path.t option -> dune_file
+
+(** Write the formatted concrete syntax to the file at [path] *)
+val write_file : path:Path.t -> Dune_lang.Cst.t list -> unit
+
 (** Reformat a dune file. [None] corresponds to stdin. *)
 val format_file : input:Path.t option -> unit
 
diff --git a/test/blackbox-tests/dune.inc b/test/blackbox-tests/dune.inc
index a63c000efe1..e5d00998c2f 100644
--- a/test/blackbox-tests/dune.inc
+++ b/test/blackbox-tests/dune.inc
@@ -191,6 +191,14 @@
    test-cases/dune-build-dir-exec-1101
    (progn (run %{exe:cram.exe} -test run.t) (diff? run.t run.t.corrected)))))
 
+(alias
+ (name dune-init)
+ (deps (package dune) (source_tree test-cases/dune-init))
+ (action
+  (chdir
+   test-cases/dune-init
+   (progn (run %{exe:cram.exe} -test run.t) (diff? run.t run.t.corrected)))))
+
 (alias
  (name dune-jbuild-var-case)
  (deps (package dune) (source_tree test-cases/dune-jbuild-var-case))
@@ -1371,6 +1379,7 @@
   (alias dir-target-dep)
   (alias double-echo)
   (alias dune-build-dir-exec-1101)
+  (alias dune-init)
   (alias dune-jbuild-var-case)
   (alias dune-package)
   (alias dune-ppx-driver-system)
@@ -1536,6 +1545,7 @@
   (alias dir-target-dep)
   (alias double-echo)
   (alias dune-build-dir-exec-1101)
+  (alias dune-init)
   (alias dune-jbuild-var-case)
   (alias dune-package)
   (alias dune-project-edition)
diff --git a/test/blackbox-tests/test-cases/dune-init/existing_project/bin/main.ml b/test/blackbox-tests/test-cases/dune-init/existing_project/bin/main.ml
new file mode 100644
index 00000000000..05c2a25a77e
--- /dev/null
+++ b/test/blackbox-tests/test-cases/dune-init/existing_project/bin/main.ml
@@ -0,0 +1 @@
+() = print_endline "Goodbye"
diff --git a/test/blackbox-tests/test-cases/dune-init/existing_project/src/dune b/test/blackbox-tests/test-cases/dune-init/existing_project/src/dune
new file mode 100644
index 00000000000..c6ac264cd57
--- /dev/null
+++ b/test/blackbox-tests/test-cases/dune-init/existing_project/src/dune
@@ -0,0 +1,5 @@
+; A comment
+
+(library
+ ; Another comment
+ (name test_lib))
diff --git a/test/blackbox-tests/test-cases/dune-init/run.t b/test/blackbox-tests/test-cases/dune-init/run.t
new file mode 100644
index 00000000000..1bae590d357
--- /dev/null
+++ b/test/blackbox-tests/test-cases/dune-init/run.t
@@ -0,0 +1,281 @@
+Adding a library
+----------------
+
+Can init a public library
+
+  $ dune init lib test_lib ./_test_lib_dir --public
+  Success: initialized library component named test_lib
+
+Can build the public library
+
+  $ cd _test_lib_dir && touch test_lib.opam && dune build
+  Info: creating file dune-project with this contents:
+  | (lang dune 1.9)
+  | (name test_lib)
+  
+  $ cat ./_test_lib_dir/dune
+  (library
+   (public_name test_lib)
+   (name test_lib))
+
+Clean up the library tests
+
+  $ rm -rf ./_test_lib_dir
+
+Can init library with a specified public name
+
+  $ dune init lib test_lib ./_test_lib_dir --public test_lib_public_name
+  Success: initialized library component named test_lib
+  $ cat ./_test_lib_dir/dune
+  (library
+   (public_name test_lib_public_name)
+   (name test_lib))
+
+Clean up library with specified public name
+
+  $ rm -rf ./_test_lib_dir
+
+Can add a library with inline tests
+
+  $ dune init lib test_lib ./_inline_tests_lib --inline-tests --ppx ppx_inline_tests
+  Success: initialized library component named test_lib
+  $ cat ./_inline_tests_lib/dune
+  (library
+   (inline_tests)
+   (name test_lib)
+   (preprocess
+    (pps ppx_inline_tests)))
+
+Clean up library with inlines tests
+
+  $ rm -rf ./_inline_tests_lib
+
+Adding an executable
+--------------------
+
+Can init a public executable
+
+  $ dune init exe test_bin ./_test_bin_dir --public
+  Success: initialized executable component named test_bin
+
+Can build an executable
+
+  $ cd _test_bin_dir && touch test_bin.opam && dune build
+  Info: creating file dune-project with this contents:
+  | (lang dune 1.9)
+  | (name test_bin)
+  
+
+Can run the created executable
+
+  $ cd _test_bin_dir && dune exec test_bin
+  Hello, World!
+
+Clean up the executable tests
+
+  $ rm -rf ./_test_bin_dir
+
+Adding tests
+------------
+
+Can init tests
+
+  $ dune init test test_tests ./_test_tests_dir --libs foo,bar
+  Success: initialized test component named test_tests
+  $ ls ./_test_tests_dir
+  dune
+  test_tests.ml
+  $ cat ./_test_tests_dir/dune
+  (test
+   (name test_tests)
+   (libraries foo bar))
+
+Clean up the test tests
+
+  $ rm -rf ./_test_tests_dir
+
+Adding components to default and non-standard places
+---------------------------------------------------
+
+Add a library in the current working directory
+
+  $ dune init lib test_lib
+  Success: initialized library component named test_lib
+  $ cat dune
+  (library
+   (name test_lib))
+
+Clean the library creation
+
+  $ rm ./dune
+
+Add a library to a dune file in a specified directory
+
+  $ dune init lib test_lib ./_test_dir
+  Success: initialized library component named test_lib
+  $ test -f ./_test_dir/dune
+
+Clean up from the dune file created in ./_test_dir
+
+  $ rm -rf ./_test_dir
+
+Add a library to a dune file in a directory specified with an absolute path
+
+  $ dune init lib test_lib $PWD/_test_dir
+  Success: initialized library component named test_lib
+  $ test -f $PWD/_test_dir/dune
+
+Clean up from the dune file created at an absolute path
+
+  $ rm -rf $PWD/_test_dir
+
+Adding a library and an executable dependent on that library
+------------------------------------------------------------
+
+Can init a library and dependent executable in a combo project
+
+  $ dune init lib test_lib ./_test_lib_exe_dir/src
+  Success: initialized library component named test_lib
+  $ dune init exe test_bin ./_test_lib_exe_dir/bin --libs test_lib --public
+  Success: initialized executable component named test_bin
+
+Can build the combo project
+
+  $ cd _test_lib_exe_dir && touch test_bin.opam && dune build
+  Info: creating file dune-project with this contents:
+  | (lang dune 1.9)
+  | (name test_bin)
+  
+
+Can run the combo project
+
+  $ cd _test_lib_exe_dir && dune exec test_bin
+  Hello, World!
+
+Clean up the combo project
+
+  $ rm -rf ./_test_lib_exe_dir
+
+Adding libraries in a single directory
+--------------------------------------
+
+Can add multiple libraries in the same directory
+
+  $ dune init lib test_lib1 ./_test_lib --public
+  Success: initialized library component named test_lib1
+  $ dune init lib test_lib2 ./_test_lib --libs test_lib1
+  Success: initialized library component named test_lib2
+  $ cat ./_test_lib/dune
+  (library
+   (public_name test_lib1)
+   (name test_lib1))
+  
+  (library
+   (name test_lib2)
+   (libraries test_lib1))
+
+Can build the multiple library project
+
+  $ cd _test_lib && touch test_lib1.opam && dune build
+  Info: creating file dune-project with this contents:
+  | (lang dune 1.9)
+  | (name test_lib1)
+  
+
+Clan up the multiple library project
+
+  $ rm -rf ./_test_lib
+
+Multiple ppxs and library dependencies
+--------------------------------------
+
+Can add multiple library dependencies in one command
+
+  $ dune init lib test_lib ./_test_lib --libs foo,bar --ppx ppx_foo,ppx_bar
+  Success: initialized library component named test_lib
+  $ cat _test_lib/dune
+  (library
+   (name test_lib)
+   (libraries foo bar)
+   (preprocess
+    (pps ppx_foo ppx_bar)))
+
+Clean up the multiple dependencies project
+
+  $ rm -rf ./_test_lib
+
+Safety and Validation
+---------------------
+
+Will not overwrite existing files
+
+  $ dune init exe test_bin ./existing_project/bin
+  Warning: file existing_project/bin/main.ml was not created because it already exists
+  Success: initialized executable component named test_bin
+  $ cat ./existing_project/bin/main.ml
+  () = print_endline "Goodbye"
+
+Comments in dune files are preserved
+
+  $ dune init lib test_lib2 ./existing_project/src
+  Success: initialized library component named test_lib2
+  $ cat ./existing_project/src/dune
+  ; A comment
+  
+  (library
+   ; Another comment
+   (name test_lib))
+  
+  (library
+   (name test_lib2))
+
+Will not create components with invalid names
+
+  $ dune init lib invalid-component-name ./_test_lib
+  A component named 'invalid-component-name' cannot be created because it is an invalid library name.
+  Hint: library names must be non-empty and composed only of the following characters: 'A'..'Z',  'a'..'z', '_'  or '0'..'9'
+  [1]
+  $ test -f ./_test_lib
+  [1]
+
+Will fail and inform user when invalid component command is given
+
+  $ dune init foo blah
+  dune: INIT_KIND argument: invalid value `foo', expected one of `exe', `lib'
+        or `test'
+  Usage: dune init [OPTION]... INIT_KIND NAME [PATH]
+  Try `dune init --help' or `dune --help' for more information.
+  [1]
+
+Will fail and inform user when an invalid option is given to a component
+
+  $ dune init test test_foo --public
+  The test component does not support the public option
+  [1]
+  $ dune init exe test_exe --inline-tests
+  The executable component does not support the inline-tests option
+  [1]
+
+Adding fields to existing stanzas
+---------------------------------
+
+# TODO(shonfeder)
+Adding fields to existing stanzas is currently not supported
+
+  $ dune init exe test_bin ./_test_bin --libs test_lib1 --public
+  Success: initialized executable component named test_bin
+  $ dune init exe test_bin ./_test_bin --libs test_lib2
+  Updating existing stanzas is not yet supported.
+  A preexisting dune stanza conflicts with a generated stanza:
+  
+  Generated stanza:
+  (executable (name main) (libraries test_lib2))
+  
+  Pre-existing stanza:
+  (executable (public_name test_bin) (name main) (libraries test_lib1))
+  [1]
+  $ cat ./_test_bin/dune
+  (executable
+   (public_name test_bin)
+   (name main)
+   (libraries test_lib1))