Restructure project directory, python files are now in their own folder
This commit is contained in:
0
python/file_scripts/__init__.py
Normal file
0
python/file_scripts/__init__.py
Normal file
76
python/file_scripts/editor.py
Normal file
76
python/file_scripts/editor.py
Normal 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]
|
20
python/file_scripts/error.py
Normal file
20
python/file_scripts/error.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
.. 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
|
||||
E_FILE_EXISTS = 6
|
||||
|
||||
# ----- Messages -----
|
||||
NO_FILE_SELECTED_MESSAGE = "No file was selected."
|
||||
|
||||
# ========== Functions ==========
|
66
python/file_scripts/fzf.py
Normal file
66
python/file_scripts/fzf.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""
|
||||
.. moduleauthor:: Eric Torres
|
||||
.. module:: files-scripts.fzf
|
||||
:synopsis: Helper functions for interacting with fzf
|
||||
"""
|
||||
# ========== Imports ==========
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
|
||||
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):
|
||||
"""Custom error class for any errors that occur when fzf is run.
|
||||
|
||||
**Attributes**
|
||||
|
||||
* exit_code: exit code when fzf exited
|
||||
* message: stderr of fzf's process when the error occured
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, exit_code, message):
|
||||
"""
|
||||
:type exit_code: int
|
||||
"""
|
||||
self.exit_code = exit_code
|
||||
self.message = message
|
||||
|
||||
def __repr__(self):
|
||||
return f"FZFError({self.exit_code}, {self.message})"
|
||||
|
||||
def __str__(self):
|
||||
return str(self.__repr__())
|
||||
|
||||
|
||||
# ========== 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: path of selected file
|
||||
:rtype: path-like object
|
||||
:raises: FZFError if there was an error with fzf or no file was selected
|
||||
"""
|
||||
try:
|
||||
# if we use capture_output, it captures file selection dialog
|
||||
# use subprocess.PIPE instead to prevent this
|
||||
output = subprocess.run(
|
||||
(FZF_CMD, *FZF_OPTS), input=files, stdout=subprocess.PIPE, check=True
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise FZFError(e.returncode, e.stderr) from e
|
||||
else:
|
||||
return Path(output.stdout.decode(LOCALE).strip("\x00"))
|
110
python/file_scripts/search.py
Normal file
110
python/file_scripts/search.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""
|
||||
.. moduleauthor:: Eric Torres
|
||||
.. module:: package.module
|
||||
:synopsis: Short and succinct module description
|
||||
"""
|
||||
# ========== Imports ==========
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from multiprocessing import cpu_count
|
||||
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",
|
||||
"--threads",
|
||||
str(cpu_count()),
|
||||
]
|
||||
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
|
||||
"""
|
||||
|
||||
# Sample of a command that gets passed to subprocess:
|
||||
# ['/usr/bin/find', '--hidden', '--', '<pattern>', 'path/to/directory']
|
||||
cmd = [bin_override if bin_override is not None else FIND_CMD, *opts]
|
||||
|
||||
cmd.append("--")
|
||||
|
||||
""" Options Matrix
|
||||
pattern T T F F
|
||||
directory T F T F
|
||||
1 2 3 4
|
||||
|
||||
1. Pattern and directory were given: pass both
|
||||
2. Only pattern was given: pass pattern and give . as directory
|
||||
3. Only directory was given: pass . as pattern and give directory
|
||||
4. Neither pattern nor directory were given: do nothing
|
||||
"""
|
||||
if pattern is not None and directory is not None:
|
||||
cmd.extend([pattern, directory])
|
||||
elif pattern is not None and directory is None:
|
||||
cmd.extend([pattern, "."])
|
||||
elif pattern is None and directory is not None:
|
||||
cmd.extend([".", directory])
|
||||
elif pattern is None and directory is None:
|
||||
pass
|
||||
|
||||
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
|
Reference in New Issue
Block a user