What it does
edits.sh connects to a remote server via SFTP, pulls files into a local temp directory, and opens them in your preferred editor. When you save and close a file, it is pushed back to the remote. Files closed without saving are skipped. The temp directory is cleaned up on exit.
edits.sh means you use your prefered editor with your personal configurations: your keybindings, themes, plugins, LSP, and snippets. It is a lightweight alternative to configuring a full remote development environment for quick edits on servers you don't control or connect to very often.
Usage
chmod +x edits.sh
sudo cp edits.sh /usr/local/bin/edits
edits user@host:/path/to/dir to edit a
directory, or
edits user@host:/path/file.txt to
target a single file. Run with no argument to be
prompted.
Flags
| -e ext |
Extension(s) to pull and edit. Defaults to
txt. Pass a comma-separated list to
match multiple types: -e sh,py,conf.
|
| -r | Recurse into subdirectories. Pulls matching files from the full directory tree under the given remote path. |
| -y | Skip the confirmation prompt and begin editing immediately. |
| $EDITOR |
Respects the $EDITOR environment
variable. Defaults to nano if unset.
|
Source
#!/usr/bin/env bash # edits.sh - pull remote files, edit locally, push back on save # chmod +x edits.sh - make script executable # sudo cp edits.sh /usr/local/bin/edits # credit - barney matthews (www.barney.me) set -euo pipefail EDITOR="${EDITOR:-nano}" EXTS="txt" RECURSIVE=false YES=false # Parse flags while [[ $# -gt 0 ]]; do case "$1" in -e) EXTS="$2"; shift 2 ;; -r) RECURSIVE=true; shift ;; -y) YES=true; shift ;; *) REMOTE="$1"; shift ;; esac done echo "" echo "edits.sh [-e ext[,ext]] [-r] [-y] [user@host:/path]" echo " -e ext Extension(s) to edit, comma-separated (default: txt)" echo " -r Recurse into subdirectories" echo " -y Skip confirmation prompt" echo " Save and close a file to upload it. Close without saving to skip." echo "" # Prompt if no remote provided if [[ -z "${REMOTE:-}" ]]; then read -rp "Remote path (user@host:/path): " REMOTE fi REMOTE_HOST="${REMOTE%%:*}" REMOTE_PATH="${REMOTE#*:}" TMPDIR="$(mktemp -d)" trap 'rm -rf "$TMPDIR"' EXIT # Detect single-file vs directory target if [[ "$REMOTE_PATH" == *.* ]] && sftp -q "$REMOTE_HOST" <<SFTP 2>/dev/null lcd "$TMPDIR" get "$REMOTE_PATH" SFTP then FILES=("$TMPDIR"/"$(basename "$REMOTE_PATH")") else # Build sftp mget commands for each extension SFTP_CMDS="lcd \"$TMPDIR\"\ncd \"$REMOTE_PATH\"" IFS=',' read -ra EXT_LIST <<< "$EXTS" for ext in "${EXT_LIST[@]}"; do ext="${ext// /}" if [[ "$RECURSIVE" == true ]]; then SFTP_CMDS+="\nfind . -name \"*.$ext\" | while read f; do get \"\$f\"; done" else SFTP_CMDS+="\nmget *.$ext" fi done sftp -q "$REMOTE_HOST" <<< "$(echo -e "$SFTP_CMDS")" || true # Collect all pulled files FILES=() for ext in "${EXT_LIST[@]}"; do ext="${ext// /}" while IFS= read -r f; do FILES+=("$f") done < <(find "$TMPDIR" -name "*.$ext" -type f) done fi if [[ ${#FILES[@]} -eq 0 ]]; then echo "No matching files found." >&2 exit 1 fi echo "Found ${#FILES[@]} file(s): $(basename -a "${FILES[@]}" | tr '\n' ' ')" echo "" # Edit each file; track which ones changed CHANGED=() for f in "${FILES[@]}"; do MTIME_BEFORE="$(stat -c %Y "$f" 2>/dev/null || stat -f %m "$f")" "$EDITOR" "$f" MTIME_AFTER="$(stat -c %Y "$f" 2>/dev/null || stat -f %m "$f")" if [[ "$MTIME_BEFORE" != "$MTIME_AFTER" ]]; then CHANGED+=("$f") else echo "No changes to $(basename "$f"), skipping." fi done # Batch upload all changed files in one SFTP session if [[ ${#CHANGED[@]} -gt 0 ]]; then echo "" echo "Uploading ${#CHANGED[@]} file(s) ..." UPLOAD_CMDS="cd \"$REMOTE_PATH\"" for f in "${CHANGED[@]}"; do UPLOAD_CMDS+="\nput \"$f\"" done sftp -q "$REMOTE_HOST" <<< "$(echo -e "$UPLOAD_CMDS")" echo "Done." fi