diff --git a/README.md b/README.md
index 5468e7a..c425c15 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,7 @@ Features
4. Manual validation and linting of manifests (see [Flycheck][] for on-the-fly
validation and linting)
5. Integration with [Puppet Debugger][]
+6. Skeletons for many standard Puppet statements and resource declarations
Installation
------------
@@ -48,20 +49,39 @@ manifests with the extension `.pp`.
The following key bindings are available in Puppet Mode:
-Key | Command
--------------------|--------------------------------------------------
-C-M-a | Move to the beginning of the current block
-C-M-e | Move to the end of the current block
-C-c C-a | Align parameters in the current block
-C-c C-' | Toggle string quoting between single and double
-C-c C-; | Blank the string at point
-C-c C-j | Jump to a `class`, `define`, variable or resource
-C-c C-c | Apply the current manifest in dry-run mode
-C-c C-v | Validate the syntax of the current manifest
-C-c C-l | Check the current manifest for semantic issues
-C-c C-z | Launch a puppet-debugger REPL
-C-c C-r | Send the currently marked region to the REPL
-C-c C-b | Send the current buffer to the REPL
+Key | Command
+---------------------|--------------------------------------------------
+C-M-a | Move to the beginning of the current block
+C-M-e | Move to the end of the current block
+C-c C-a | Align parameters in the current block
+C-c C-' | Toggle string quoting between single and double
+C-c C-; | Blank the string at point
+C-c C-j | Jump to a `class`, `define`, variable or resource
+C-c C-c | Apply the current manifest in dry-run mode
+C-c C-v | Validate the syntax of the current manifest
+C-c C-l | Check the current manifest for semantic issues
+C-c C-z | Launch a puppet-debugger REPL
+C-c C-r | Send the currently marked region to the REPL
+C-c C-b | Send the current buffer to the REPL
+C-c C-k c | Insert `class` definition skeleton
+C-c C-k d | Insert `define` definition skeleton
+C-c C-k n | Insert `node` definition skeleton
+C-c C-k i | Insert `if` statement skeleton
+C-c C-k e | Insert `elsif` statement skeleton
+C-c C-k o | Insert `else` statement skeleton
+C-c C-k u | Insert `unless` statement skeleton
+C-c C-k s | Insert `case` statement skeleton
+C-c C-k ? | Insert `selector` statement skeleton
+C-c C-t a | Insert `anchor` resource skeleton
+C-c C-t c | Insert `class` resource skeleton
+C-c C-t e | Insert `exec` resource skeleton
+C-c C-t f | Insert `file` resource skeleton
+C-c C-t g | Insert `group` resource skeleton
+C-c C-t h | Insert `host` resource skeleton
+C-c C-t n | Insert `notify` resource skeleton
+C-c C-t p | Insert `package` resource skeleton
+C-c C-t s | Insert `service` resource skeleton
+C-c C-t u | Insert `user` resource skeleton
For the integration with puppet-debugger to work, the puppet-debugger gem needs
diff --git a/puppet-mode.el b/puppet-mode.el
index a7b503b..ffb69b5 100644
--- a/puppet-mode.el
+++ b/puppet-mode.el
@@ -105,6 +105,8 @@ buffer-local wherever it is set."
(declare-function pkg-info-version-info "pkg-info" (library))
(eval-when-compile
+ (require 'cl-macs)
+ (require 'skeleton)
(require 'rx))
(require 'align)
@@ -1127,6 +1129,189 @@ With a prefix argument SUPPRESS it simply inserts $."
(delete-region (+ min 1) (- max 1)))))
+
+;;; Skeletons
+
+(defun puppet-dissect-filename (file)
+ "Return list of path components for the Puppet manifest FILE.
+The first element of the list will be the module name and the
+remaining elements are the relative path components below the
+‘manifests’ subdirectory. The names of the path components are
+only derived from the file name by using the Puppet auto-loader
+rules. FILE must be an absolute file name.
+
+The module name \"unidentified\" is returned if a module name
+can't be inferred from the file name.
+
+If the directory name contains characters that are not legal for
+a Puppet module name, then all leading characters including the
+last illegal character are removed from the module name. The
+function will for example return ‘foo’ as module name even if the
+module is using the ‘puppet-foo’ directory (e.g. for module
+development in a user's home directory)."
+ (if (stringp file)
+ (let* ((parts (cl-loop for path = file then (directory-file-name
+ (file-name-directory path))
+ ;; stop iteration at the root of the directory
+ ;; tree (should work for Windows & Unix/Linux)
+ until (or (string-suffix-p ":" path)
+ (string-equal (file-name-directory path)
+ path))
+ collect (file-name-base path)))
+ ;; Remove "init" if it is the first element
+ (compact (if (string-equal (car parts) "init")
+ (cdr parts)
+ parts)))
+ (cons
+ ;; module name with illegal prefixes removed or "unidentified" if
+ ;; path is not compliant with the standard Puppet file hierarchy
+ (replace-regexp-in-string
+ "^.*[^a-z0-9_]" "" (or (cadr (member "manifests" parts))
+ "unidentified"))
+ ;; remaining path components
+ (cdr (member "manifests" (reverse compact)))))
+ '("unidentified")))
+
+(defun puppet-file-module-name (file)
+ "Return the module name for the Puppet class in FILE."
+ (car (puppet-dissect-filename file)))
+
+(defun puppet-file-class-name (file)
+ "Return the class name for the Puppet class in FILE."
+ (mapconcat #'identity (puppet-dissect-filename file) "::"))
+
+(define-skeleton puppet-keyword-class
+ "Insert \"class\" skeleton."
+ nil
+ "class " (puppet-file-class-name (buffer-file-name)) " (" > \n
+ ") {" > \n
+ > _ "}" > \n)
+
+(define-skeleton puppet-keyword-define
+ "Insert \"class\" skeleton."
+ nil
+ "define " (puppet-file-class-name (buffer-file-name)) " (" > \n
+ ") {" > \n
+ > _ "}" > \n)
+
+(define-skeleton puppet-keyword-node
+ "Insert \"node\" skeleton."
+ nil
+ "node " > - " {" \n
+ > _ "}" > \n)
+
+(define-skeleton puppet-keyword-if
+ "Insert \"if\" statement."
+ nil
+ "if " > - " {" \n
+ > _ "}" > \n)
+
+(define-skeleton puppet-keyword-elsif
+ "Insert \"elsif\" statement."
+ nil
+ "elsif " > - " {" \n
+ > _ "}" > \n)
+
+(define-skeleton puppet-keyword-else
+ "Insert \"else\" statement."
+ nil
+ "else {" > \n
+ > _ "}" > \n)
+
+(define-skeleton puppet-keyword-unless
+ "Insert \"unless\" statement."
+ nil
+ "unless " > - " {" \n
+ > _ "}" > \n)
+
+(define-skeleton puppet-keyword-case
+ "Insert \"case\" statement."
+ nil
+ "case " > - " {" \n
+ "default: {" > \n
+ "}" > \n
+ "}" > \n)
+
+(define-skeleton puppet-keyword-selector
+ "Insert \"?\" selector."
+ nil
+ "? {" > \n
+ "default => " > - "," \n
+ "}" > \n)
+
+(define-skeleton puppet-type-anchor
+ "Insert the \"anchor\" resource type."
+ nil
+ "anchor { " > - ": }" \n)
+
+(define-skeleton puppet-type-class
+ "Insert the \"class\" resource type."
+ nil
+ "class { " > - ":" \n
+ "}" > \n)
+
+(define-skeleton puppet-type-exec
+ "Insert the \"exec\" resource type."
+ nil
+ "exec { " > - ":" \n
+ "path => [ '/bin', '/sbin', '/usr/bin', '/usr/sbin', ]," > \n
+ "user => 'root'," > \n
+ "cwd => '/'," > \n
+ "}" > \n)
+
+(define-skeleton puppet-type-file
+ "Insert the \"file\" resource type."
+ nil
+ "file { " > - ":" \n
+ "ensure => file," > \n
+ "owner => 'root'," > \n
+ "group => 'root'," > \n
+ "mode => '0644'," > \n
+ "}" > \n)
+
+(define-skeleton puppet-type-group
+ "Insert the \"group\" resource type."
+ nil
+ "group { " > - ":" \n
+ "ensure => present," > \n
+ "}" > \n)
+
+(define-skeleton puppet-type-host
+ "Insert the \"host\" resource type."
+ nil
+ "host { " > - ":" \n
+ "ensure => present," > \n
+ "}" > \n)
+
+(define-skeleton puppet-type-notify
+ "Insert the \"notify\" resource type."
+ nil
+ "notify { " > - ": }" \n)
+
+(define-skeleton puppet-type-package
+ "Insert the \"package\" resource type."
+ nil
+ "package { " > - ":" \n
+ "ensure => present," > \n
+ "}" > \n)
+
+(define-skeleton puppet-type-service
+ "Insert the \"service\" resource type."
+ nil
+ "service { " > - ":" \n
+ "ensure => running," > \n
+ "enable => true," > \n
+ "}" > \n)
+
+(define-skeleton puppet-type-user
+ "Insert the \"user\" resource type."
+ nil
+ "user { " > - ":" \n
+ "ensure => present," > \n
+ "shell => '/bin/bash'," > \n
+ "password => '*'," > \n
+ "}" > \n)
+
;;; Imenu
@@ -1220,6 +1405,27 @@ for each entry."
;; Linting and validation
(define-key map (kbd "C-c C-v") #'puppet-validate)
(define-key map (kbd "C-c C-l") #'puppet-lint)
+ ;; Skeletons for types
+ (define-key map (kbd "C-c C-t a") #'puppet-type-anchor)
+ (define-key map (kbd "C-c C-t c") #'puppet-type-class)
+ (define-key map (kbd "C-c C-t e") #'puppet-type-exec)
+ (define-key map (kbd "C-c C-t f") #'puppet-type-file)
+ (define-key map (kbd "C-c C-t g") #'puppet-type-group)
+ (define-key map (kbd "C-c C-t h") #'puppet-type-host)
+ (define-key map (kbd "C-c C-t n") #'puppet-type-notify)
+ (define-key map (kbd "C-c C-t p") #'puppet-type-package)
+ (define-key map (kbd "C-c C-t s") #'puppet-type-service)
+ (define-key map (kbd "C-c C-t u") #'puppet-type-user)
+ ;; Skeletons for keywords
+ (define-key map (kbd "C-c C-k c") #'puppet-keyword-class)
+ (define-key map (kbd "C-c C-k d") #'puppet-keyword-define)
+ (define-key map (kbd "C-c C-k n") #'puppet-keyword-node)
+ (define-key map (kbd "C-c C-k i") #'puppet-keyword-if)
+ (define-key map (kbd "C-c C-k e") #'puppet-keyword-elsif)
+ (define-key map (kbd "C-c C-k o") #'puppet-keyword-else)
+ (define-key map (kbd "C-c C-k u") #'puppet-keyword-unless)
+ (define-key map (kbd "C-c C-k s") #'puppet-keyword-case)
+ (define-key map (kbd "C-c C-k ?") #'puppet-keyword-selector)
;; The menu bar
(easy-menu-define puppet-menu map "Puppet Mode menu"
`("Puppet"
@@ -1278,6 +1484,8 @@ for each entry."
;; Alignment
(setq align-mode-rules-list puppet-mode-align-rules)
(setq align-mode-exclude-rules-list puppet-mode-align-exclude-rules)
+ ;; Skeletons
+ (setq-local skeleton-end-newline nil)
;; IMenu
(setq imenu-create-index-function #'puppet-imenu-create-index))
diff --git a/test/puppet-mode-test.el b/test/puppet-mode-test.el
index b3e9875..5d30a90 100644
--- a/test/puppet-mode-test.el
+++ b/test/puppet-mode-test.el
@@ -615,6 +615,434 @@ class foo {
}
}"))))
+
+;;;; Skeletons
+
+(ert-deftest puppet-skeleton/puppet-dissect-filename-unix ()
+ :tags '(skeleton)
+ (should (equal (puppet-dissect-filename "/modules/foo/manifests/init.pp")
+ '("foo")))
+ (should (equal (puppet-dissect-filename "/modules/foo/manifests/bar.pp")
+ '("foo" "bar")))
+ (should (equal (puppet-dissect-filename "/modules/foo/manifests/bar/baz.pp")
+ '("foo" "bar" "baz"))))
+
+(ert-deftest puppet-skeleton/puppet-dissect-filename-windows ()
+ :tags '(skeleton)
+ (should (equal (puppet-dissect-filename "C:/modules/foo/manifests/init.pp")
+ '("foo")))
+ (should (equal (puppet-dissect-filename "C:/modules/foo/manifests/bar.pp")
+ '("foo" "bar")))
+ (should (equal (puppet-dissect-filename "C:/modules/foo/manifests/bar/baz.pp")
+ '("foo" "bar" "baz"))))
+
+(ert-deftest puppet-skeleton/puppet-dissect-filename-unidentified ()
+ :tags '(skeleton)
+ (should (equal (puppet-dissect-filename "/modules/init.pp")
+ '("unidentified")))
+ (should (equal (puppet-dissect-filename "/modules/foo/init.pp")
+ '("unidentified")))
+ (should (equal (puppet-dissect-filename "/modules/foo/bar/init.pp")
+ '("unidentified"))))
+
+(ert-deftest puppet-skeleton/puppet-dissect-filename-nil ()
+ :tags '(skeleton)
+ (should (equal (puppet-dissect-filename nil)
+ '("unidentified"))))
+
+(ert-deftest puppet-skeleton/puppet-dissect-filename-invalid-module ()
+ :tags '(skeleton)
+ (should (equal (puppet-dissect-filename "/puppet-foo/manifests/init.pp")
+ '("foo"))))
+
+(ert-deftest puppet-skeleton/keyword-class ()
+ :tags '(skeleton)
+ (puppet-test-with-temp-buffer
+ ""
+ (set-visited-file-name "/modules/foo/manifests/bar.pp" t)
+ (puppet-keyword-class)
+ (should (string= (buffer-string) "class foo::bar (
+) {
+}"))))
+
+(ert-deftest puppet-skeleton/keyword-define ()
+ :tags '(skeleton)
+ (puppet-test-with-temp-buffer
+ ""
+ (set-visited-file-name "/modules/foo/manifests/bar.pp" t)
+ (puppet-keyword-define)
+ (should (string= (buffer-string) "define foo::bar (
+) {
+}"))))
+
+(ert-deftest puppet-skeleton/keyword-node ()
+ :tags '(skeleton)
+ (puppet-test-with-temp-buffer
+ ""
+ (puppet-keyword-node)
+ (should (looking-at " {")) ; node name
+ (should (string= (buffer-string) "node {
+}"))))
+
+(ert-deftest puppet-skeleton/keyword-if-no-region ()
+ :tags '(skeleton)
+ (puppet-test-with-temp-buffer
+ "
+class foo {
+}"
+ (goto-char (point-min))
+ (forward-line 2)
+ (puppet-keyword-if)
+ (should (looking-at " {")) ; condition
+ (should (string= (buffer-string) "
+class foo {
+ if {
+ }
+}"))))
+
+(ert-deftest puppet-skeleton/keyword-if-region ()
+ :tags '(skeleton)
+ (puppet-test-with-temp-buffer
+ "
+class foo {
+ notify { 'foo': }
+ notify { 'bar': }
+}"
+ (goto-char (point-min))
+ (forward-line 2)
+ (push-mark (line-beginning-position 2) t)
+ (activate-mark)
+ (puppet-keyword-if)
+ (should (looking-at " {")) ; condition
+ (should (string= (buffer-string) "
+class foo {
+ if {
+ notify { 'foo': }
+ }
+ notify { 'bar': }
+}"))))
+
+(ert-deftest puppet-skeleton/keyword-elsif-no-region ()
+ :tags '(skeleton)
+ (puppet-test-with-temp-buffer
+ "
+class foo {
+}"
+ (goto-char (point-min))
+ (forward-line 2)
+ (puppet-keyword-elsif)
+ (should (looking-at " {")) ; condition
+ (should (string= (buffer-string) "
+class foo {
+ elsif {
+ }
+}"))))
+
+(ert-deftest puppet-skeleton/keyword-elsif-region ()
+ :tags '(skeleton)
+ (puppet-test-with-temp-buffer
+ "
+class foo {
+ notify { 'foo': }
+ notify { 'bar': }
+}"
+ (goto-char (point-min))
+ (forward-line 2)
+ (push-mark (line-beginning-position 2) t)
+ (activate-mark)
+ (puppet-keyword-elsif)
+ (should (looking-at " {")) ; condition
+ (should (string= (buffer-string) "
+class foo {
+ elsif {
+ notify { 'foo': }
+ }
+ notify { 'bar': }
+}"))))
+
+(ert-deftest puppet-skeleton/keyword-else-no-region ()
+ :tags '(skeleton)
+ (puppet-test-with-temp-buffer
+ "
+class foo {
+}"
+ (goto-char (point-min))
+ (forward-line 2)
+ (puppet-keyword-else)
+ (should (string= (buffer-string) "
+class foo {
+ else {
+ }
+}"))))
+
+(ert-deftest puppet-skeleton/keyword-else-region ()
+ :tags '(skeleton)
+ (puppet-test-with-temp-buffer
+ "
+class foo {
+ notify { 'foo': }
+ notify { 'bar': }
+}"
+ (goto-char (point-min))
+ (forward-line 2)
+ (push-mark (line-beginning-position 2) t)
+ (activate-mark)
+ (puppet-keyword-else)
+ (should (string= (buffer-string) "
+class foo {
+ else {
+ notify { 'foo': }
+ }
+ notify { 'bar': }
+}"))))
+
+(ert-deftest puppet-skeleton/keyword-unless-no-region ()
+ :tags '(skeleton)
+ (puppet-test-with-temp-buffer
+ "
+class foo {
+}"
+ (goto-char (point-min))
+ (forward-line 2)
+ (puppet-keyword-unless)
+ (should (looking-at " {")) ; condition
+ (should (string= (buffer-string) "
+class foo {
+ unless {
+ }
+}"))))
+
+(ert-deftest puppet-skeleton/keyword-unless-region ()
+ :tags '(skeleton)
+ (puppet-test-with-temp-buffer
+ "
+class foo {
+ notify { 'foo': }
+ notify { 'bar': }
+}"
+ (goto-char (point-min))
+ (forward-line 2)
+ (push-mark (line-beginning-position 2) t)
+ (activate-mark)
+ (puppet-keyword-unless)
+ (should (looking-at " {")) ; condition
+ (should (string= (buffer-string) "
+class foo {
+ unless {
+ notify { 'foo': }
+ }
+ notify { 'bar': }
+}"))))
+
+(ert-deftest puppet-skeleton/keyword-case ()
+ :tags '(skeleton)
+ (puppet-test-with-temp-buffer
+ "
+class foo {
+}"
+ (goto-char (point-min))
+ (forward-line 2)
+ (puppet-keyword-case)
+ (should (looking-at " {")) ; expression
+ (should (string= (buffer-string) "
+class foo {
+ case {
+ default: {
+ }
+ }
+}"))))
+
+(ert-deftest puppet-skeleton/keyword-selector ()
+ :tags '(skeleton)
+ (puppet-test-with-temp-buffer
+ "
+class foo {
+}"
+ (goto-char (point-min))
+ (forward-line 2)
+ (insert " $foo ")
+ (puppet-keyword-selector)
+ (should (looking-at ",")) ; value
+ (should (string= (buffer-string) "
+class foo {
+ $foo ? {
+ default => ,
+ }
+}"))))
+
+(ert-deftest puppet-skeleton/type-anchor ()
+ :tags '(skeleton)
+ (puppet-test-with-temp-buffer
+ "
+class foo {
+}"
+ (goto-char (point-min))
+ (forward-line 2)
+ (puppet-type-anchor)
+ (should (looking-at ":")) ; title
+ (should (string= (buffer-string) "
+class foo {
+ anchor { : }
+}"))))
+
+(ert-deftest puppet-skeleton/type-class ()
+ :tags '(skeleton)
+ (puppet-test-with-temp-buffer
+ "
+class foo {
+}"
+ (goto-char (point-min))
+ (forward-line 2)
+ (puppet-type-class)
+ (should (looking-at ":")) ; title
+ (should (string= (buffer-string) "
+class foo {
+ class { :
+ }
+}"))))
+
+(ert-deftest puppet-skeleton/type-exec ()
+ :tags '(skeleton)
+ (puppet-test-with-temp-buffer
+ "
+class foo {
+}"
+ (goto-char (point-min))
+ (forward-line 2)
+ (puppet-type-exec)
+ (should (looking-at ":")) ; title
+ (should (string= (buffer-string) "
+class foo {
+ exec { :
+ path => [ '/bin', '/sbin', '/usr/bin', '/usr/sbin', ],
+ user => 'root',
+ cwd => '/',
+ }
+}"))))
+
+(ert-deftest puppet-skeleton/type-file ()
+ :tags '(skeleton)
+ (puppet-test-with-temp-buffer
+ "
+class foo {
+}"
+ (goto-char (point-min))
+ (forward-line 2)
+ (puppet-type-file)
+ (should (looking-at ":")) ; title
+ (should (string= (buffer-string) "
+class foo {
+ file { :
+ ensure => file,
+ owner => 'root',
+ group => 'root',
+ mode => '0644',
+ }
+}"))))
+
+(ert-deftest puppet-skeleton/type-group ()
+ :tags '(skeleton)
+ (puppet-test-with-temp-buffer
+ "
+class foo {
+}"
+ (goto-char (point-min))
+ (forward-line 2)
+ (puppet-type-group)
+ (should (looking-at ":")) ; title
+ (should (string= (buffer-string) "
+class foo {
+ group { :
+ ensure => present,
+ }
+}"))))
+
+(ert-deftest puppet-skeleton/type-host ()
+ :tags '(skeleton)
+ (puppet-test-with-temp-buffer
+ "
+class foo {
+}"
+ (goto-char (point-min))
+ (forward-line 2)
+ (puppet-type-host)
+ (should (looking-at ":")) ; title
+ (should (string= (buffer-string) "
+class foo {
+ host { :
+ ensure => present,
+ }
+}"))))
+
+(ert-deftest puppet-skeleton/type-notify ()
+ :tags '(skeleton)
+ (puppet-test-with-temp-buffer
+ "
+class foo {
+}"
+ (goto-char (point-min))
+ (forward-line 2)
+ (puppet-type-notify)
+ (should (looking-at ":")) ; title
+ (should (string= (buffer-string) "
+class foo {
+ notify { : }
+}"))))
+
+(ert-deftest puppet-skeleton/type-package ()
+ :tags '(skeleton)
+ (puppet-test-with-temp-buffer
+ "
+class foo {
+}"
+ (goto-char (point-min))
+ (forward-line 2)
+ (puppet-type-package)
+ (should (looking-at ":")) ; title
+ (should (string= (buffer-string) "
+class foo {
+ package { :
+ ensure => present,
+ }
+}"))))
+
+(ert-deftest puppet-skeleton/type-service ()
+ :tags '(skeleton)
+ (puppet-test-with-temp-buffer
+ "
+class foo {
+}"
+ (goto-char (point-min))
+ (forward-line 2)
+ (puppet-type-service)
+ (should (looking-at ":")) ; title
+ (should (string= (buffer-string) "
+class foo {
+ service { :
+ ensure => running,
+ enable => true,
+ }
+}"))))
+
+(ert-deftest puppet-skeleton/type-user ()
+ :tags '(skeleton)
+ (puppet-test-with-temp-buffer
+ "
+class foo {
+}"
+ (goto-char (point-min))
+ (forward-line 2)
+ (puppet-type-user)
+ (should (looking-at ":")) ; title
+ (should (string= (buffer-string) "
+class foo {
+ user { :
+ ensure => present,
+ shell => '/bin/bash',
+ password => '*',
+ }
+}"))))
+
;;;; Imenu