Initial commit

This commit is contained in:
Eric Torres
2020-10-23 11:21:59 -07:00
parent 478cd22c07
commit 1cb66c8834
15 changed files with 762 additions and 0 deletions

0
file_scripts/__init__.py Normal file
View File

76
file_scripts/editor.py Normal file
View File

@ -0,0 +1,76 @@
"""
.. moduleauthor:: Eric Torres
.. module:: file_scripts.editor
:synopsis: Helper functions for interacting with system editors
"""
# ========== Imports ==========
import os
import shutil
from pathlib import Path
# ========== Constants ==========
SUDO_COMMAND = "sudo"
# ========== Functions ==========
def select_editor(override=None):
"""Return a canonical path to an editor.
Select an editor from one of:
* -e, --editor
* $EDITOR
* Default of vim
In this order
If an editor cannot be resolved, then a FileNotFoundError is raised instead.
:param override: argument to override an editor
:returns: path to one of these editors
:rtype: str
:raises: FileNotFoundError if an editor could not be resolved
"""
editor = None
if override is not None:
editor = shutil.which(override)
elif "EDITOR" in os.environ:
editor = shutil.which(os.environ.get("EDITOR"))
elif shutil.which("vim") is not None:
editor = shutil.which("vim")
if editor is None:
raise FileNotFoundError("An editor could not be resolved")
return editor
def gen_editor_cmd(editor, filename):
"""Generate a command line to run for editing a file based on
permissions. This command line is suitable for passing to
subprocess.run().
This command does not pass extra options to the editor, hence
there are no arguments to pass for options.
Conditions
----------
* Path exists: use normal command
* Path does not exist: use normal command
* Path exists but is not readable by current process: use SUDO_COMMAND
:param editor: path of editor
:type editor: str or path-like object
:param filename: path/name of file to edit
:type filename: str or path-like object
:returns: command to execute to edit file
:rtype: list
"""
# Possible for a race condition to occur here
# What happens if the file or its metadata changes?
path = Path(filename)
if path.exists() and not os.access(path, os.W_OK):
return [SUDO_COMMAND, "--edit", filename]
else:
return [editor, filename]

19
file_scripts/error.py Normal file
View File

@ -0,0 +1,19 @@
"""
.. moduleauthor:: Eric Torres
.. module:: file_scripts:error
:synopsis: Exit codes and error handling.
"""
# ========== Imports ==========
# ========== Constants ==========
# ----- Exit Codes -----
E_INTERRUPT = 1
E_NOFILESELECTED = 2
E_NOEDITORFOUND = 3
E_NO_RESULTS = 4
E_USER_RESPONSE_NO = 5
# ----- Messages -----
NO_FILE_SELECTED_MESSAGE = "No file was selected."
# ========== Functions ==========

45
file_scripts/fzf.py Normal file
View File

@ -0,0 +1,45 @@
"""
.. moduleauthor:: Eric Torres
.. module:: files-scripts.fzf
:synopsis: Helper functions for interacting with fzf
"""
# ========== Imports ==========
import shutil
import subprocess
import file_scripts.error as error
from pathlib import Path
# ========== Constants ==========
# ----- Commands -----
# Options: read null terminator, auto-select if one option, exit if no options, print null terminator
FZF_CMD = shutil.which("fzf")
FZF_OPTS = ["--read0", "--select-1", "--exit-0", "--print0"]
# ----- Misc. -----
LOCALE = "utf-8"
class FZFError(Exception):
pass
# ========== Functions ==========
def select_file_with_fzf(files):
"""Run fzf on a stream of searched files for the user to select.
:param files: stream of null-terminated files to read
:type files: bytes stream (stdout of a completed process)
:returns: selected file
:rtype: path-like object
:raises: FZFError if there was an error with fzf or no file was selected
"""
output = subprocess.run([FZF_CMD, *FZF_OPTS], input=files, stdout=subprocess.PIPE)
try:
output.check_returncode()
except subprocess.CalledProcessError as e:
raise FZFError(error.NO_FILE_SELECTED_MESSAGE) from e
else:
return Path(selected_file.stdout.decode(LOCALE).strip("\x00"))

80
file_scripts/search.py Normal file
View File

@ -0,0 +1,80 @@
"""
.. moduleauthor:: Eric Torres
.. module:: package.module
:synopsis: Short and succinct module description
"""
# ========== Imports ==========
import shutil
import subprocess
from sys import platform
# ========== Constants ==========
# ----- Commands -----
# Options: show hidden files, null terminator, files only
# Optional arguments: show vcs files, show every file
FIND_CMD = shutil.which("fd")
DEFAULT_FIND_OPTS = ["--hidden", "--print0", "--type", "f", "--type", "l"]
EXTRA_FIND_OPTS = {"no_ignore": "--no-ignore", "no_ignore_vcs": "--no-ignore-vcs"}
# Options: null terminator, ignore case, print names matching all non-option arguments
LOCATE_CMD = shutil.which("locate")
# Platform-specific options
# macOS doesn't support GNU-style long options
LOCATE_OPTS = []
if platform == "linux":
LOCATE_OPTS = ["--all", "--ignore-case", "--null"]
elif platform == "darwin":
LOCATE_OPTS = ["-0", "-i"]
# ========== Functions ==========
def find_files(
opts=DEFAULT_FIND_OPTS,
directory=None,
capture_text=None,
bin_override=None,
pattern=None,
):
"""Use a find-based program to locate files. The returned data is
the stdout of the completed process.
:param opts: options to pass to the find program
:type opts: list of str
:param directory: directory to search for files
:type directory: str
:param capture_text: capture output as text
:type capture_text: bool
:param bin_override: override find binary
:type bin_override: str
:param pattern: patterns to pass to fd
:type pattern: str
:returns: path of user-selected file
:rtype: bytes, str if capture_text was initialized from None
"""
cmd = [bin_override if bin_override is not None else FIND_CMD, *opts]
cmd.append("--")
if pattern is not None:
cmd.append(pattern)
cmd.append(directory if directory is not None else ".")
return subprocess.run(cmd, capture_output=True, text=capture_text).stdout
def locate_files(patterns, bin_override=None, capture_text=None):
"""Use a locate-based program to locate files, then pass to fzf.
:param patterns: patterns to pass to locate
:type patterns: list
:param bin_override: override find binary
:type bin_override: str
:param capture_text: capture output as text
:type capture_text: bool
:returns: path of user-selected file
:rtype: bytes, str if capture_text was initialized from None
"""
cmd = [bin_override if bin_override is not None else LOCATE_CMD, *LOCATE_OPTS]
cmd.extend(patterns)
return subprocess.run(cmd, capture_output=True, text=capture_text).stdout