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