dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

f.el (18374B)


      1 ;;; f.el --- Modern API for working with files and directories -*- lexical-binding: t; -*-
      2 
      3 ;; Copyright (C) 2013 Johan Andersson
      4 
      5 ;; Author: Johan Andersson <johan.rejeep@gmail.com>
      6 ;; Maintainer: Johan Andersson <johan.rejeep@gmail.com>
      7 ;; Version: 0.20.0
      8 ;; Package-Version: 0.20.0
      9 ;; Package-Commit: de6d4d40ddc844eee643e92d47b9d6a63fbebb48
     10 ;; Keywords: files, directories
     11 ;; URL: http://github.com/rejeep/f.el
     12 ;; Package-Requires: ((s "1.7.0") (dash "2.2.0"))
     13 
     14 ;; This file is NOT part of GNU Emacs.
     15 
     16 ;;; License:
     17 
     18 ;; This program is free software; you can redistribute it and/or modify
     19 ;; it under the terms of the GNU General Public License as published by
     20 ;; the Free Software Foundation; either version 3, or (at your option)
     21 ;; any later version.
     22 
     23 ;; This program is distributed in the hope that it will be useful,
     24 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     25 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     26 ;; GNU General Public License for more details.
     27 
     28 ;; You should have received a copy of the GNU General Public License
     29 ;; along with GNU Emacs; see the file COPYING.  If not, write to the
     30 ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
     31 ;; Boston, MA 02110-1301, USA.
     32 
     33 ;;; Code:
     34 
     35 
     36 
     37 (require 's)
     38 (require 'dash)
     39 
     40 (put 'f-guard-error 'error-conditions '(error f-guard-error))
     41 (put 'f-guard-error 'error-message "Destructive operation outside sandbox")
     42 
     43 (defvar f--guard-paths nil
     44   "List of allowed paths to modify when guarded.
     45 
     46 Do not modify this variable.")
     47 
     48 (defmacro f--destructive (path &rest body)
     49   "If PATH is allowed to be modified, yield BODY.
     50 
     51 If PATH is not allowed to be modified, throw error."
     52   (declare (indent 1))
     53   `(if f--guard-paths
     54        (if (--any? (or (f-same? it ,path)
     55                        (f-ancestor-of? it ,path)) f--guard-paths)
     56            (progn ,@body)
     57          (signal 'f-guard-error (list ,path f--guard-paths)))
     58      ,@body))
     59 
     60 
     61 ;;;; Paths
     62 
     63 (defun f-join (&rest args)
     64   "Join ARGS to a single path."
     65   (let (path (relative (f-relative? (car args))))
     66     (-map
     67      (lambda (arg)
     68        (setq path (f-expand arg path)))
     69      args)
     70     (if relative (f-relative path) path)))
     71 
     72 (defun f-split (path)
     73   "Split PATH and return list containing parts."
     74   (let ((parts (s-split (f-path-separator) path 'omit-nulls)))
     75     (if (f-absolute? path)
     76         (push (f-path-separator) parts)
     77       parts)))
     78 
     79 (defun f-expand (path &optional dir)
     80   "Expand PATH relative to DIR (or `default-directory').
     81 PATH and DIR can be either a directory names or directory file
     82 names.  Return a directory name if PATH is a directory name, and
     83 a directory file name otherwise.  File name handlers are
     84 ignored."
     85   (let (file-name-handler-alist)
     86     (expand-file-name path dir)))
     87 
     88 (defun f-filename (path)
     89   "Return the name of PATH."
     90   (file-name-nondirectory (directory-file-name path)))
     91 
     92 (defalias 'f-parent 'f-dirname)
     93 (defun f-dirname (path)
     94   "Return the parent directory to PATH."
     95   (let ((parent (file-name-directory
     96                  (directory-file-name (f-expand path default-directory)))))
     97     (unless (f-same? path parent)
     98       (if (f-relative? path)
     99           (f-relative parent)
    100         (directory-file-name parent)))))
    101 
    102 (defun f-common-parent (paths)
    103   "Return the deepest common parent directory of PATHS."
    104   (cond
    105    ((not paths) nil)
    106    ((not (cdr paths)) (f-parent (car paths)))
    107    (:otherwise
    108     (let* ((paths (-map 'f-split paths))
    109            (common (caar paths))
    110            (re nil))
    111       (while (and (not (null (car paths))) (--all? (equal (car it) common) paths))
    112         (setq paths (-map 'cdr paths))
    113         (push common re)
    114         (setq common (caar paths)))
    115       (cond
    116        ((null re) "")
    117        ((and (= (length re) 1) (f-root? (car re)))
    118         (f-root))
    119        (:otherwise
    120         (concat (apply 'f-join (nreverse re)) "/")))))))
    121 
    122 (defun f-ext (path)
    123   "Return the file extension of PATH.
    124 
    125 The extension, in a file name, is the part that follows the last
    126 '.', excluding version numbers and backup suffixes."
    127   (file-name-extension path))
    128 
    129 (defun f-no-ext (path)
    130   "Return everything but the file extension of PATH."
    131   (file-name-sans-extension path))
    132 
    133 (defun f-swap-ext (path ext)
    134   "Return PATH but with EXT as the new extension.
    135 EXT must not be nil or empty."
    136   (if (s-blank? ext)
    137       (error "Extension cannot be empty or nil")
    138     (concat (f-no-ext path) "." ext)))
    139 
    140 (defun f-base (path)
    141   "Return the name of PATH, excluding the extension of file."
    142   (f-no-ext (f-filename path)))
    143 
    144 (defun f-relative (path &optional dir)
    145   "Return PATH relative to DIR."
    146   (file-relative-name path dir))
    147 
    148 (defalias 'f-abbrev 'f-short)
    149 (defun f-short (path)
    150   "Return abbrev of PATH.  See `abbreviate-file-name'."
    151   (abbreviate-file-name path))
    152 
    153 (defun f-long (path)
    154   "Return long version of PATH."
    155   (f-expand path))
    156 
    157 (defun f-canonical (path)
    158   "Return the canonical name of PATH."
    159   (file-truename path))
    160 
    161 (defun f-slash (path)
    162   "Append slash to PATH unless one already.
    163 
    164 Some functions, such as `call-process' requires there to be an
    165 ending slash."
    166   (if (f-dir? path)
    167       (file-name-as-directory path)
    168     path))
    169 
    170 (defun f-full (path)
    171   "Return absolute path to PATH, with ending slash."
    172   (f-slash (f-long path)))
    173 
    174 (defun f--uniquify (paths)
    175   "Helper for `f-uniquify' and `f-uniquify-alist'."
    176   (let* ((files-length (length paths))
    177          (uniq-filenames (--map (cons it (f-filename it)) paths))
    178          (uniq-filenames-next (-group-by 'cdr uniq-filenames)))
    179     (while (/= files-length (length uniq-filenames-next))
    180       (setq uniq-filenames-next
    181             (-group-by 'cdr
    182                        (--mapcat
    183                         (let ((conf-files (cdr it)))
    184                           (if (> (length conf-files) 1)
    185                               (--map (cons (car it) (concat (f-filename (s-chop-suffix (cdr it) (car it))) (f-path-separator) (cdr it))) conf-files)
    186                             conf-files))
    187                         uniq-filenames-next))))
    188     uniq-filenames-next))
    189 
    190 (defun f-uniquify (files)
    191   "Return unique suffixes of FILES.
    192 
    193 This function expects no duplicate paths."
    194   (-map 'car (f--uniquify files)))
    195 
    196 (defun f-uniquify-alist (files)
    197   "Return alist mapping FILES to unique suffixes of FILES.
    198 
    199 This function expects no duplicate paths."
    200   (-map 'cadr (f--uniquify files)))
    201 
    202 
    203 ;;;; I/O
    204 
    205 (defun f-read-bytes (path)
    206   "Read binary data from PATH.
    207 
    208 Return the binary data as unibyte string."
    209   (with-temp-buffer
    210     (set-buffer-multibyte nil)
    211     (setq buffer-file-coding-system 'binary)
    212     (insert-file-contents-literally path)
    213     (buffer-substring-no-properties (point-min) (point-max))))
    214 
    215 (defalias 'f-read 'f-read-text)
    216 (defun f-read-text (path &optional coding)
    217   "Read text with PATH, using CODING.
    218 
    219 CODING defaults to `utf-8'.
    220 
    221 Return the decoded text as multibyte string."
    222   (decode-coding-string (f-read-bytes path) (or coding 'utf-8)))
    223 
    224 (defalias 'f-write 'f-write-text)
    225 (defun f-write-text (text coding path)
    226   "Write TEXT with CODING to PATH.
    227 
    228 TEXT is a multibyte string.  CODING is a coding system to encode
    229 TEXT with.  PATH is a file name to write to."
    230   (f-write-bytes (encode-coding-string text coding) path))
    231 
    232 (defun f-unibyte-string-p (s)
    233   "Determine whether S is a unibyte string."
    234   (not (multibyte-string-p s)))
    235 
    236 (defun f-write-bytes (data path)
    237   "Write binary DATA to PATH.
    238 
    239 DATA is a unibyte string.  PATH is a file name to write to."
    240   (f--destructive path
    241     (unless (f-unibyte-string-p data)
    242       (signal 'wrong-type-argument (list 'f-unibyte-string-p data)))
    243     (let ((file-coding-system-alist nil)
    244           (coding-system-for-write 'binary))
    245       (with-temp-file path
    246         (setq buffer-file-coding-system 'binary)
    247         (set-buffer-multibyte nil)
    248         (insert data)))))
    249 
    250 (defalias 'f-append 'f-append-text)
    251 (defun f-append-text (text coding path)
    252   "Append TEXT with CODING to PATH.
    253 
    254 If PATH does not exist, it is created."
    255   (f-append-bytes (encode-coding-string text coding) path))
    256 
    257 (defun f-append-bytes (data path)
    258   "Append binary DATA to PATH.
    259 
    260 If PATH does not exist, it is created."
    261   (let ((content
    262          (if (f-file? path)
    263              (f-read-bytes path)
    264            "")))
    265     (f-write-bytes (concat content data) path)))
    266 
    267 
    268 ;;;; Destructive
    269 
    270 (defun f-mkdir (&rest dirs)
    271   "Create directories DIRS."
    272   (let (path)
    273     (-each
    274         dirs
    275       (lambda (dir)
    276         (setq path (f-expand dir path))
    277         (unless (f-directory? path)
    278           (f--destructive path (make-directory path)))))))
    279 
    280 (defun f-delete (path &optional force)
    281   "Delete PATH, which can be file or directory.
    282 
    283 If FORCE is t, a directory will be deleted recursively."
    284   (f--destructive path
    285     (if (or (f-file? path) (f-symlink? path))
    286         (delete-file path)
    287       (delete-directory path force))))
    288 
    289 (defun f-symlink (source path)
    290   "Create a symlink to SOURCE from PATH."
    291   (f--destructive path (make-symbolic-link source path)))
    292 
    293 (defun f-move (from to)
    294   "Move or rename FROM to TO.
    295 If TO is a directory name, move FROM into TO."
    296   (f--destructive to (rename-file from to t)))
    297 
    298 (defun f-copy (from to)
    299   "Copy file or directory FROM to TO.
    300 If FROM names a directory and TO is a directory name, copy FROM
    301 into TO as a subdirectory."
    302   (f--destructive to
    303     (if (f-file? from)
    304         (copy-file from to)
    305       ;; The behavior of `copy-directory' differs between Emacs 23 and
    306       ;; 24 in that in Emacs 23, the contents of `from' is copied to
    307       ;; `to', while in Emacs 24 the directory `from' is copied to
    308       ;; `to'. We want the Emacs 24 behavior.
    309       (if (> emacs-major-version 23)
    310           (copy-directory from to)
    311         (if (f-dir? to)
    312             (progn
    313               (apply 'f-mkdir (f-split to))
    314               (let ((new-to (f-expand (f-filename from) to)))
    315                 (copy-directory from new-to)))
    316           (copy-directory from to))))))
    317 
    318 (defun f-copy-contents (from to)
    319   "Copy contents in directory FROM, to directory TO."
    320   (unless (f-exists? to)
    321     (error "Cannot copy contents to non existing directory %s" to))
    322   (unless (f-dir? from)
    323     (error "Cannot copy contents as %s is a file" from))
    324   (--each (f-entries from)
    325     (f-copy it (file-name-as-directory to))))
    326 
    327 (defun f-touch (path)
    328   "Update PATH last modification date or create if it does not exist."
    329   (f--destructive path
    330     (if (f-file? path)
    331         (set-file-times path)
    332       (f-write-bytes "" path))))
    333 
    334 
    335 ;;;; Predicates
    336 
    337 (defun f-exists? (path)
    338   "Return t if PATH exists, false otherwise."
    339   (file-exists-p path))
    340 
    341 (defalias 'f-exists-p 'f-exists?)
    342 
    343 (defalias 'f-dir? 'f-directory?)
    344 (defalias 'f-dir-p 'f-dir?)
    345 
    346 (defun f-directory? (path)
    347   "Return t if PATH is directory, false otherwise."
    348   (file-directory-p path))
    349 
    350 (defalias 'f-directory-p 'f-directory?)
    351 
    352 (defun f-file? (path)
    353   "Return t if PATH is file, false otherwise."
    354   (file-regular-p path))
    355 
    356 (defalias 'f-file-p 'f-file?)
    357 
    358 (defun f-symlink? (path)
    359   "Return t if PATH is symlink, false otherwise."
    360   (not (not (file-symlink-p path))))
    361 
    362 (defalias 'f-symlink-p 'f-symlink?)
    363 
    364 (defun f-readable? (path)
    365   "Return t if PATH is readable, false otherwise."
    366   (file-readable-p path))
    367 
    368 (defalias 'f-readable-p 'f-readable?)
    369 
    370 (defun f-writable? (path)
    371   "Return t if PATH is writable, false otherwise."
    372   (file-writable-p path))
    373 
    374 (defalias 'f-writable-p 'f-writable?)
    375 
    376 (defun f-executable? (path)
    377   "Return t if PATH is executable, false otherwise."
    378   (file-executable-p path))
    379 
    380 (defalias 'f-executable-p 'f-executable?)
    381 
    382 (defun f-absolute? (path)
    383   "Return t if PATH is absolute, false otherwise."
    384   (file-name-absolute-p path))
    385 
    386 (defalias 'f-absolute-p 'f-absolute?)
    387 
    388 (defun f-relative? (path)
    389   "Return t if PATH is relative, false otherwise."
    390   (not (f-absolute? path)))
    391 
    392 (defalias 'f-relative-p 'f-relative?)
    393 
    394 (defun f-root? (path)
    395   "Return t if PATH is root directory, false otherwise."
    396   (not (f-parent path)))
    397 
    398 (defalias 'f-root-p 'f-root?)
    399 
    400 (defun f-ext? (path &optional ext)
    401   "Return t if extension of PATH is EXT, false otherwise.
    402 
    403 If EXT is nil or omitted, return t if PATH has any extension,
    404 false otherwise.
    405 
    406 The extension, in a file name, is the part that follows the last
    407 '.', excluding version numbers and backup suffixes."
    408   (if ext
    409       (string= (f-ext path) ext)
    410     (not (eq (f-ext path) nil))))
    411 
    412 (defalias 'f-ext-p 'f-ext?)
    413 
    414 (defalias 'f-equal? 'f-same?)
    415 (defalias 'f-equal-p 'f-equal?)
    416 
    417 (defun f-same? (path-a path-b)
    418   "Return t if PATH-A and PATH-B are references to same file."
    419   (when (and (f-exists? path-a)
    420              (f-exists? path-b))
    421     (equal
    422      (f-canonical (directory-file-name (f-expand path-a)))
    423      (f-canonical (directory-file-name (f-expand path-b))))))
    424 
    425 (defalias 'f-same-p 'f-same?)
    426 
    427 (defun f-parent-of? (path-a path-b)
    428   "Return t if PATH-A is parent of PATH-B."
    429   (--when-let (f-parent path-b)
    430     (f-same? path-a it)))
    431 
    432 (defalias 'f-parent-of-p 'f-parent-of?)
    433 
    434 (defun f-child-of? (path-a path-b)
    435   "Return t if PATH-A is child of PATH-B."
    436   (--when-let (f-parent path-a)
    437     (f-same? it path-b)))
    438 
    439 (defalias 'f-child-of-p 'f-child-of?)
    440 
    441 (defun f-ancestor-of? (path-a path-b)
    442   "Return t if PATH-A is ancestor of PATH-B."
    443   (unless (f-same? path-a path-b)
    444     (s-starts-with? (f-full path-a)
    445                     (f-full path-b))))
    446 
    447 (defalias 'f-ancestor-of-p 'f-ancestor-of?)
    448 
    449 (defun f-descendant-of? (path-a path-b)
    450   "Return t if PATH-A is desendant of PATH-B."
    451   (unless (f-same? path-a path-b)
    452     (s-starts-with? (f-full path-b)
    453                     (f-full path-a))))
    454 
    455 (defalias 'f-descendant-of-p 'f-descendant-of?)
    456 
    457 (defun f-hidden? (path)
    458   "Return t if PATH is hidden, nil otherwise."
    459   (unless (f-exists? path)
    460     (error "Path does not exist: %s" path))
    461   (string= (substring path 0 1) "."))
    462 
    463 (defalias 'f-hidden-p 'f-hidden?)
    464 
    465 (defun f-empty? (path)
    466   "If PATH is a file, return t if the file in PATH is empty, nil otherwise.
    467 If PATH is directory, return t if directory has no files, nil otherwise."
    468   (if (f-directory? path)
    469       (equal (f-files path nil t) nil)
    470     (= (f-size path) 0)))
    471 
    472 (defalias 'f-empty-p 'f-empty?)
    473 
    474 
    475 ;;;; Stats
    476 
    477 (defun f-size (path)
    478   "Return size of PATH.
    479 
    480 If PATH is a file, return size of that file.  If PATH is
    481 directory, return sum of all files in PATH."
    482   (if (f-directory? path)
    483       (-sum (-map 'f-size (f-files path nil t)))
    484     (nth 7 (file-attributes path))))
    485 
    486 (defun f-depth (path)
    487   "Return the depth of PATH.
    488 
    489 At first, PATH is expanded with `f-expand'.  Then the full path is used to
    490 detect the depth.
    491 '/' will be zero depth,  '/usr' will be one depth.  And so on."
    492   (- (length (f-split (f-expand path))) 1))
    493 
    494 
    495 ;;;; Misc
    496 
    497 (defun f-this-file ()
    498   "Return path to this file."
    499   (cond
    500    (load-in-progress load-file-name)
    501    ((and (boundp 'byte-compile-current-file) byte-compile-current-file)
    502     byte-compile-current-file)
    503    (:else (buffer-file-name))))
    504 
    505 (defvar f--path-separator nil
    506   "A variable to cache result of `f-path-separator'.")
    507 
    508 (defun f-path-separator ()
    509   "Return path separator."
    510   (or f--path-separator
    511       (setq f--path-separator (substring (f-join "x" "y") 1 2))))
    512 
    513 (defun f-glob (pattern &optional path)
    514   "Find PATTERN in PATH."
    515   (file-expand-wildcards
    516    (f-join (or path default-directory) pattern)))
    517 
    518 (defun f--collect-entries (path recursive)
    519   (let (result
    520         (entries
    521          (-reject
    522           (lambda (file)
    523             (or
    524              (equal (f-filename file) ".")
    525              (equal (f-filename file) "..")))
    526           (directory-files path t))))
    527     (cond (recursive
    528            (-map
    529             (lambda (entry)
    530               (if (f-file? entry)
    531                   (setq result (cons entry result))
    532                 (when (f-directory? entry)
    533                   (setq result (cons entry result))
    534                   (setq result (append result (f--collect-entries entry recursive))))))
    535             entries))
    536           (t (setq result entries)))
    537     result))
    538 
    539 (defmacro f--entries (path body &optional recursive)
    540   "Anaphoric version of `f-entries'."
    541   `(f-entries
    542     ,path
    543     (lambda (path)
    544       (let ((it path))
    545         ,body))
    546     ,recursive))
    547 
    548 (defun f-entries (path &optional fn recursive)
    549   "Find all files and directories in PATH.
    550 
    551 FN - called for each found file and directory.  If FN returns a thruthy
    552 value, file or directory will be included.
    553 RECURSIVE - Search for files and directories recursive."
    554   (let ((entries (f--collect-entries path recursive)))
    555     (if fn (-select fn entries) entries)))
    556 
    557 (defmacro f--directories (path body &optional recursive)
    558   "Anaphoric version of `f-directories'."
    559   `(f-directories
    560     ,path
    561     (lambda (path)
    562       (let ((it path))
    563         ,body))
    564     ,recursive))
    565 
    566 (defun f-directories (path &optional fn recursive)
    567   "Find all directories in PATH.  See `f-entries'."
    568   (let ((directories (-select 'f-directory? (f--collect-entries path recursive))))
    569     (if fn (-select fn directories) directories)))
    570 
    571 (defmacro f--files (path body &optional recursive)
    572   "Anaphoric version of `f-files'."
    573   `(f-files
    574     ,path
    575     (lambda (path)
    576       (let ((it path))
    577         ,body))
    578     ,recursive))
    579 
    580 (defun f-files (path &optional fn recursive)
    581   "Find all files in PATH.  See `f-entries'."
    582   (let ((files (-select 'f-file? (f--collect-entries path recursive))))
    583     (if fn (-select fn files) files)))
    584 
    585 (defmacro f--traverse-upwards (body &optional path)
    586   "Anaphoric version of `f-traverse-upwards'."
    587   `(f-traverse-upwards
    588     (lambda (dir)
    589       (let ((it dir))
    590         ,body))
    591     ,path))
    592 
    593 (defun f-traverse-upwards (fn &optional path)
    594   "Traverse up as long as FN return nil, starting at PATH.
    595 
    596 If FN returns a non-nil value, the path sent as argument to FN is
    597 returned.  If no function callback return a non-nil value, nil is
    598 returned."
    599   (unless path
    600     (setq path default-directory))
    601   (when (f-relative? path)
    602     (setq path (f-expand path)))
    603   (if (funcall fn path)
    604       path
    605     (unless (f-root? path)
    606       (f-traverse-upwards fn (f-parent path)))))
    607 
    608 (defun f-root ()
    609   "Return absolute root."
    610   (f-traverse-upwards 'f-root?))
    611 
    612 (defmacro f-with-sandbox (path-or-paths &rest body)
    613   "Only allow PATH-OR-PATHS and decendants to be modified in BODY."
    614   (declare (indent 1))
    615   `(let ((paths (if (listp ,path-or-paths)
    616                     ,path-or-paths
    617                   (list ,path-or-paths))))
    618      (unwind-protect
    619          (let ((f--guard-paths paths))
    620            ,@body)
    621        (setq f--guard-paths nil))))
    622 
    623 (provide 'f)
    624 
    625 ;;; f.el ends here