Hack 8. Don t Save Bad Perl

Hack 8. Don't Save Bad Perl

Don't even write out your file if the Perl isn't valid!

Perl tests tend to start by checking that your code compiles. Even if the tests don't check, you'll know it pretty quickly as all your code collapses in a string of compiler errors. Then you have to fire up your editor again and track down the problem. It's simple, though, to tell Vim that if your Perl code won't compile, it shouldn't even write it to disk.

Even better, you can load Perl's error messages back into Vim to jump right to the problem spots.

The Hack

Vim supports filetype plug-ins that alter its behavior based on the type of file being edited. Enable these by adding a line to your .vimrc:

filetype plugin on

Now you can put files in ~/.vim/ftplugin (My Documents\\_vimfiles\\ftplugin on Windows) and Vim will load them when it needs them. Perl plug-ins start with perl_, so save the following file as perl_synwrite.vim:

" perl_synwrite.vim: check syntax of Perl before writing
" latest version at: http://www.vim.org/scripts/script.php?script_id=896

"" abort if b:did_perl_synwrite is true: already loaded or user pref
if exists("b:did_perl_synwrite")
let b:did_perl_synwrite = 1

"" set buffer :au pref: if defined globally, inherit; otherwise, false
if (exists("perl_synwrite_au") && !exists("b:perl_synwrite_au"))
  let b:perl_synwrite_au = perl_synwrite_au
elseif !exists("b:perl_synwrite_au")
  let b:perl_synwrite_au = 0

"" set buffer quickfix pref: if defined globally, inherit; otherwise, false
if (exists("perl_synwrite_qf") && !exists("b:perl_synwrite_qf"))
  let b:perl_synwrite_qf = perl_synwrite_qf
elseif !exists("b:perl_synwrite_qf")
  let b:perl_synwrite_qf = 0

"" execute the given do_command if the buffer is syntactically correct perl
"" -- or if do_anyway is true
function! s:PerlSynDo(do_anyway,do_command)
  let command = "!perl -c"

  if (b:perl_synwrite_qf)
    " this env var tells Vi::QuickFix to replace "-" with actual filename
    let $VI_QUICKFIX_SOURCEFILE=expand("%")
    let command = command . " -MVi::QuickFix"

  " respect taint checking
  if (match(getline(1), "^#!.\\\\+perl.\\\\+-T") = = 0)
    let command = command . " -T"

  exec "write" command

  silent! cgetfile " try to read the error file
  if !v:shell_error || a:do_anyway
    exec a:do_command
    set nomod

"" set up the autocommand, if b:perl_synwrite_au is true
if (b:perl_synwrite_au > 0)
  let b:undo_ftplugin = "au! perl_synwrite * " . expand("%")

  augroup perl_synwrite
    exec "au BufWriteCmd,FileWriteCmd " . expand("%") . 
         " call s:PerlSynDo(0,\\"write <afile>\\")"
  augroup END

"" the :Write command
command -buffer -nargs=* -complete=file -range=% -bang Write call \\
    s:PerlSynDo("<bang>"= ="!","<line1>,<line2>write<bang> <args>")

Running the Hack

When you edit a Perl file, use :W instead of :w to write the file. If the file fails to compile with perl -c, Vim will refuse to write the file to disk. You can always fall back to :w, or use :W! to check, but write out the file even if it has bad syntax.

Hacking the Hack

The plug-in has two configurable options that you can set in your .vimrc. The first is perl_synwrite_au, which hooks the :W command's logic onto an autocommand that fires when you use :w. This will let you use :w for any sort of file, but still enjoy the syntax error catching of the plug-in. It's a little twitchy, though, when you start passing arguments to :w, so you're probably best off just using :W.

The second is perl_synwrite_qf, which lets the plug-in use the Vi::QuickFix module to parse perl's errors into a format that Vim can use to jump to problems. With this option set, perl will write errors to error.err, which Vim will read when you use its QuickFix commands. :help quickfix lists all of the commands, but the two most useful are :cf to jump to the first syntax error and :copen to open a new window listing all your errors. In that new window, you can move to the error that interests you, hit Enter, and jump to the error in your buffer.

Vi::QuickFix relies on tying the standard error stream, which isn't possible in Perl 5.6, so if you use perl_synwrite.vim in more than one development environment, you might want to set the perl_synwrite_qf option dynamically:

silent call system("perl -e0 -MVi::QuickFix")
let perl_synwrite_qf = ! v:shell_error

In other words, if Perl can't use the Vi::QuickFix module, don't try using it for the plug-in.

By default, Vim thinks that *.t files are Tads, or maybe Nroff, files. It's easy to fix; create a file in ~/.vim/ftdetect containing:

au BufRead,BufNewFile *.t
               set ft=perl

Now when you edit 00-load.t, Vim will know it's Perl and not your latest interactive fiction masterpiece.

Emacs users, you can use a minor mode to run a Perl syntax check before saving the file. Whenever perl -c fails, Emacs will not save your file. To save files anyway, toggle the mode off with M-x perl-syntax-mode. See "Enforce Local Style" [Hack #7] for a related tip on automatically tidying your code when saving.

(defvar perl-syntax-bin "perl"
    "The perl binary used to check syntax.")
  (defun perl-syntax-check-only ()
    "Returns a either nil or t depending on whether \\
     the current buffer passes perl's syntax check."
    (let ((buf (get-buffer-create "*Perl syntax check*")))
      (let ((syntax-ok (= 0 (save-excursion
                               (point-min) (point-max) perl-syntax-bin nil buf nil "-c"))) ))
        (if syntax-ok (kill-buffer buf)
          (display-buffer buf))
  (defvar perl-syntax-mode nil
    "Check perl syntax before saving.")
  (make-variable-buffer-local 'perl-syntax-mode)
  (defun perl-syntax-write-hook ()
    "Check perl syntax during 'write-file-hooks' for 'perl-syntax-mode'"
    (if perl-syntax-mode
          (not (perl-syntax-check-only)))
  (defun perl-syntax-mode (&optional arg)
    "Perl syntax checking minor mode."
    (interactive "P")
    (setq perl-syntax-mode
          (if (null arg)
              (not perl-syntax-mode)
            (> (prefix-numeric-value arg) 0)))
    (make-local-hook 'write-file-hooks)
    (if perl-syntax-mode
        (add-hook 'write-file-hooks 'perl-syntax-write-hook)
      (remove-hook 'write-file-hooks 'perl-syntax-write-hook)))
  (if (not (assq 'perl-syntax-mode minor-mode-alist))
      (setq minor-mode-alist
            (cons '(perl-syntax-mode " Perl Syntax")
  (eval-after-load "cperl-mode"
    '(add-hook 'cperl-mode-hook 'perl-syntax-mode))