Skip to content

Instantly share code, notes, and snippets.

@Zash
Last active November 9, 2024 01:10
Show Gist options
  • Save Zash/6c2b621a4d889c4e92e16d0efe66554a to your computer and use it in GitHub Desktop.
Save Zash/6c2b621a4d889c4e92e16d0efe66554a to your computer and use it in GitHub Desktop.
In-place editing of json/yaml
#!/bin/bash
# Yaml EDIT
# Uses https://github.com/mikefarah/yq/ installed as yq4
# Optionally uses https://github.com/junegunn/fzf
set -euo pipefail
declare file=""
declare editformat="yaml"
declare origformat=""
declare editsuffix=""
declare path="." # path to what to edit
declare load="load" # set to load_str when editing a text field
declare gron="false"
declare -i indent=2
# TODO declare -i tabs=0
# How to get yq to respect tabs?
while getopts 'e:p:' flag; do
case "$flag" in
e)
editformat="$OPTARG"
;;
p)
origformat="$OPTARG"
;;
*)
echo "$0 [-e editformat] [-p originalformat] '.path.to.something' ./file.{json,yaml}" >&2
exit 1
esac
done
shift $((OPTIND-1))
case $# in
1)
# edit a json (or anything yq supports) file as yaml
# yedit file.json
file="$1";
;;
2)
# edit a subset of a file
# yedit .path file.yaml
path="$1"
file="$2"
;;
0|*)
echo "$0 [-e editformat] [-p originalformat] '.path.to.something' ./file.{json,yaml}" >&2
exit 1
;;
esac
if [[ "$origformat" == "" ]]; then
origformat="${file##*.}"
fi
if [[ "$path" == "!" ]]; then
if ! command -v fzf >/dev/null 2>/dev/null ; then
echo fzf missing >&2
exit 1
fi
case "$editformat" in
text)
path="$(yq4 --input-format="$origformat" --output-format=tsv '.. | select(tag == "!!str") | path | with(.[] | select(to_string|match("[^A-Za-z0-9_$]")); .|= (to_json|sub("\*","\\*") | sub("\n",""))) | "."+join(".")' "$file" | fzf --reverse --height 20%)"
;;
csv|tsv)
path="$(yq4 --input-format="$origformat" --output-format=tsv '.. | select(tag=="!!seq") | path | with(.[] | select(to_string|match("[^A-Za-z0-9_$]")); .|= (to_json|sub("\*","\\*") | sub("\n",""))) | "."+join(".")' "$file" | fzf --reverse --height 20%)"
;;
*)
# editing scalars will not behave correctly
path="$(yq4 --input-format="$origformat" --output-format=tsv '.. | select(tag == "!!map" or tag=="!!seq") | path | with(.[] | select(to_string|match("[^A-Za-z0-9_$]")); .|= (to_json|sub("\*","\\*") | sub("\n",""))) | "."+join(".")' "$file" | fzf --reverse --height 20%)"
;;
esac
fi
if [[ "$editformat" == "$origformat" ]] && [[ "$path" == "." ]]; then
# edit whole file in the same format, just invoke editor
exec sensible-editor "$file"
fi
if [[ "$editformat" == "text" ]]; then
editformat="yaml"
editsuffix="md"
load="load_str"
fi
if [[ "$editformat" == "gron" ]]; then
if ! command -v gron >/dev/null 2>/dev/null ; then
echo gron missing >&2
exit 1
fi
gron="true"
editformat="json"
editsuffix="js"
fi
if command -v editorconfig >/dev/null 2>/dev/null ; then
indent="$(editorconfig "$(readlink -f "$file")" | grep '^indent_size=[0-9]' | cut -d= -f2 || echo 2)"
# tabs="$(editorconfig "$(readlink -f "$file")" | grep '^indent_style=' | cut -d= -f2 || echo default)"
fi
declare tempfile
tempfile="$(mktemp --suffix=".${editsuffix:-$editformat}")"
origfile="$(mktemp --suffix=".orig")"
cleanup() {
rm -f "$tempfile" "$origfile"
}
trap cleanup EXIT
if ! yq4 --exit-status --no-colors --csv-auto-parse=false --tsv-auto-parse=false --input-format="$origformat" --output-format="$editformat" --expression="$path" "$file" > "$tempfile"; then
read -n 1 -p "No value, edit anyway? [yn] " -r answer
echo >&2
if [[ "$answer" != y ]]; then
exit 1
fi
fi
if [[ "$gron" == "true" ]]; then
gron "$tempfile" > "$origfile"
mv "$origfile" "$tempfile"
fi
# Normalise
if [[ "$editformat" == "yml" ]]; then
editformat="yaml"
fi
if [[ "$origformat" == "yml" ]]; then
origformat="yaml"
fi
# Inject a comment with the filename and path to give some context
declare orig_comment=""
if [[ "$editformat" == "yaml" ]]; then
orig_comment="$(yq4 '. | head_comment' "$tempfile")"
file="$file" path="$path" yq4 -i '. head_comment = strenv(file) + "\n" + strenv(path) + "\n" + head_comment' "$tempfile"
fi
cp -a "$tempfile" "$origfile"
# Invoke the editor
sensible-editor "$tempfile"
# Check if file was changed
if diff -q >/dev/null "$origfile" "$tempfile"; then
echo 'no change' >&2
exit
fi
# Reset comment
if [[ "$editformat" == "yaml" && "$origformat" == "yaml" ]]; then
comment="$orig_comment" yq4 -i '. head_comment = strenv(comment)' "$tempfile"
fi
if [[ "$gron" == "true" ]]; then
gron -u "$tempfile" > "$origfile"
cp "$origfile" "$tempfile"
fi
if [[ "$load" != "load_str" ]]; then
yq4 --exit-status --inplace --csv-auto-parse=false --tsv-auto-parse=false --input-format="$editformat" --output-format="yaml" "$tempfile"
fi
export tempfile
if test -f "$file"; then
yq4 --exit-status --inplace --indent=$indent --csv-auto-parse=false --tsv-auto-parse=false --input-format="$origformat" --output-format="$origformat" --expression "$path = $load(strenv(tempfile))" "$file"
else
yq4 --exit-status --null-input --indent=$indent --csv-auto-parse=false --tsv-auto-parse=false --input-format="$origformat" --output-format="$origformat" --expression "$path = $load(strenv(tempfile))" > "$file"
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment