Compare commits

...
6 Commits
Author SHA1 Message Date
Riyyi 9a4a5e8d74 Completion: Update manager completion to the new argument structure 2021-08-24 03:40:11 +02:00
Riyyi f4a3779b04 Manager: Implement path filtering to pushing and pulling
It now possible to filter the pushing and pulling functions on
zero, one or multiple files and/or directories.
2021-08-24 03:12:30 +02:00
Riyyi 8f69bed7d9 Manager: Check if no operation is selected, improve error formatting 2021-08-22 22:34:12 +02:00
Riyyi 14a0d810ee Manager: Update help to Groff and to reflect new argument structure
Change the help function output to use the official man utility, which
uses Groff (GNU troff) formatting.
2021-08-22 20:58:36 +02:00
Riyyi 2e06b13a69 Manager: Major restructure of arguments
Optional arguments with spaces aren't supported by GNU getopt. So
instead lets restructure the arguments to make sense without them.
The manager now supports stdin.

This patch also separates file pulling and pushing into separate
functions.
2021-08-22 19:41:30 +02:00
Riyyi 80cf6138bc Manager: Change local variables to camelCase 2021-08-21 20:30:57 +02:00
2 changed files with 361 additions and 124 deletions
+125 -17
View File
@@ -1,25 +1,133 @@
#compdef dotfiles.sh #compdef dotfiles.sh
_dotfiles.sh() { typeset -A opt_args
integer ret=1
local -a args
args+=( # Argument definition
'*'{-a,--add=}'[add file to the dotfiles directory]:file:_files' # ------------------------------------------
'(-f --files)'{-f,--files}'[display all files added to the dotfiles directory]'
_dotfiles_operations=(
{-F-,--file}'[display all files added to the dotfiles directory]'
{-P-,--package}'[instal, print or store packages]'
'(-)'{-h,--help}'[display usage message and exit]' '(-)'{-h,--help}'[display usage message and exit]'
'(-p --packages)'{-p,--packages=}'[instal, list or store packages]:package functions:(( )
install\:"install all core packages of the stored list"
install-aur\:"install all AUR packages of the stored list"
list\:"display all packages installed on the system (default)"
store\:"stores a list of all installed packages"
))'
'(-l --pull)'{-l,--pull}'[pull each added file from system to dotfiles directory]'
'(-s --push)'{-s,--push}'[push each added file to its location on the system]'
)
_arguments -s -S $args[@] && ret=0 _dotfiles_file_options=(
return ret '(-F --file)'{-F,--file}
'(-a --add)'{-a,--add}'[add selected file paths to the dotfiles directory]'
'(-l --pull)'{-l,--pull}'[pull file(s) from the system to the dotfiles directory]'
'(-s --push)'{-s,--push}'[push file(s) from the dotfiles directory to the system]'
)
_dotfiles_package_options=(
'(-P --package)'{-P,--package}
'(-a --aur-install)'{-a,--aur-install}'[install all AUR packages of the stored list]'
'(-i --install)'{-i,--install}'[install all official packages of the stored list]'
'(-s --store)'{-s,--store}'[stores a list of all installed packages on the system]'
)
_dotfiles_dotfile_directory_target=(
'*:file:_files'
)
_dotfiles_home_directory_target=(
'*:file:_files -W ~/'
)
# Argument handling
# ------------------------------------------
function _dotfiles_file_argument_handling()
{
case "$tmp" in
-*)
_arguments -s : "$_dotfiles_file_options[@]" && result=0
;;
*)
# Enable matching of dotfiles without explicitly specifying the dot
setopt globdots
case "$args" in
*a*)
_arguments -s : "$_dotfiles_home_directory_target[@]" && result=0
;;
*)
_arguments -s : "$_dotfiles_dotfile_directory_target[@]" && result=0
;;
esac
;;
esac
}
function _dotfiles_package_argument_handling()
{
case "$tmp" in
-*)
_arguments -s : "$_dotfiles_package_options[@]" && result=0
;;
*)
case "$args" in
P)
_arguments -s : '*:package:_dotfiles_completions_tracked_packages' && result=0
;;
*)
;;
esac
;;
esac
}
if ! zmodload -s zsh/mapfile; then
function _dotfiles_completions_tracked_packages() {}
else
function _dotfiles_completions_tracked_packages()
{
local -a packages
packages=( "${(f@)${mapfile[packages]%$'\n'}}" )
readonly packages
compadd "$@" -a packages
}
fi
# Completion control flow
# ------------------------------------------
_dotfiles.sh()
{
local result=1
local -a args
local tmp
# Get all short flags
args=( ${${${(M)words:#-*}#-}:#-*} )
# Get the current flag
tmp="$words[-1]"
case "$args" in
h*)
_message -e arguments "no more arguments"
;;
F*)
_dotfiles_file_argument_handling
;;
P*)
_dotfiles_package_argument_handling
;;
*)
case ${(M)words:#--*} in
*--file*)
_dotfiles_file_argument_handling
;;
*--package*)
_dotfiles_package_argument_handling
;;
*)
_arguments -s : "$_dotfiles_operations[@]" && result=0
;;
esac
;;
esac
return result
} }
_dotfiles.sh _dotfiles.sh
+236 -107
View File
@@ -1,9 +1,10 @@
#!/bin/sh #!/bin/sh
# Manages dotfiles and packages # Tracks dotfiles and packages
# Depends: GNU getopt, (pacman, pacman-contrib) / (dpkg, apt) # Depends: GNU getopt, (pacman, pacman-contrib) / (dpkg, apt)
# User-config--------------------------- # User-config
# --------------------------------------
# File which holds all installed packages # File which holds all installed packages
packageFile="packages" packageFile="packages"
@@ -19,79 +20,143 @@ aurHelper="trizen"
# -------------------------------------- # --------------------------------------
b="$(tput bold)"
red="$(tput setf 4)"
n="$(tput sgr0)"
if [ "$(dirname "$0")" != "." ]; then if [ "$(dirname "$0")" != "." ]; then
echo "Please run this script from the directory it resides." >&2 echo "${b}${red}Error:${n} please run this script from the directory it resides." >&2
exit 1 exit 1
fi fi
help() help()
{ {
B=$(tput bold) (cat << EOF
U=$(tput smul) .TH DOTFILES.SH 1 "2021-08-22" "dotfiles.sh 0.9" "dotfiles.sh Manual"
N=$(tput sgr0)
cat << EOF .SH NAME
${B}NAME${N} dotfiles.sh \- config file and package tracking utility
dotfiles - manages dotfiles and packages
${B}SYNOPSIS${N} .SH SYNOPSIS
${B}./dotfiles.sh${N} ${U}OPTION${N} [${U}ARG${N}] .B ./dotfiles.sh
.I OPERATION
.RI [ OPTION ...]\&
.RI [ TARGET ...]
${B}OPTIONS${N} .SH DESCRIPTION
${B}-a${N} ${U}FILE${N}, ${B}--add${N}=${U}FILE${N} dotfiles.sh is a config file and package tracking utility that tracks installed packages on a Linux system.
Add file to the dotfiles directory. It features listing and tracking of config files and packages, and the ability to install the tracked packages.
${B}-f, --files${N} Currently, package tracking is only supported on APT and Pacman based distributions.
Display all files added to the dotfiles directory.
${B}-h, --help${N} Invoking dotfile.sh involves specifying an operation with any potential options and targets to operate on.
Display usage message and exit. A \fItarget\fR is usually a file name, directory or a package name.
Targets can be provided as command line arguments.
Additionally, if a single hyphen (-) is passed as an argument, targets will be read from stdin.
${B}-p${N} [${U}FUNCTION${N}], ${B}--packages${N}=[${U}FUNCTION${N}] .SH OPERATIONS
Apply ${U}FUNCTION${N} to the package manager packages. .TP
.BR \-F ", " \-\-file
Operate on config files.
This operation allows you to sync config files between the system and the dotfiles directory.
In the first case, if no file names are provided in the command line, all files will be selected.
See File Options below.
${U}install${N} Install all core packages of the stored list. .TP
.BR \-P ", " \-\-package
Operate on packages.
This operation allows you to track installed packages and reinstall them.
In the first case, if no package names are provided in the command line, all packages will be selected.
See Package Options below.
${U}install-aur${N} Install all AUR packages of the stored list. .TP
.BR \-h ", " \-\-help
Display usage message and exit.
${U}list${N} Display all packages installed on the system. .SH FILE OPTIONS (APPLY TO -F)
.TP
.BR \-a ", " \-\-add
Add selected file \fIpaths\fR to the dotfiles directory.
${U}store${N} Stores a list of all installed packages on the system. .TP
.BR \-l ", " \-\-pull
Pull every (selected) \fIfile\fR from the system to the dotfiles directory.
The default value is ${U}list${N}. .TP
.BR \-s ", " \-\-push
Push every (selected) \fIfile\fR from the dotfiles directory to the system.
${B}-l, --pull${N} .SH PACKAGE OPTIONS (APPLY TO -P)
Pull each added file from the system to the dotfiles directory. .TP
.BR \-a ", " \-\-aur-install
Install all AUR packages of the stored list.
${B}-s, --push${N} .TP
Push each added file to its location on the system. .BR \-i ", " \-\-install
Install all official packages of the stored list.
.TP
.BR \-s ", " \-\-store
Stores a list of all installed packages on the system.
.SH EXAMPLES
Usage examples:
$ \fB./dotfiles.sh\fR -Fa ~/.zshrc /etc/zsh/zshenv
.br
\h'4'Add config files to the dotfiles directory
$ \fB./dotfiles.sh\fR -Pia
.br
\h'4'Install all tracked official and AUR packages
.SH AUTHOR
Riyyi
EOF EOF
) | man -l -
} }
# Exit if no option is provided # Exit if no option is provided
[ "$#" -eq 0 ] && help >&2 && exit 1 [ "$#" -eq 0 ] && help && exit 1
# Files # Files
# -------------------------------------- # --------------------------------------
setFiles() getFileList()
{ {
files="$(find . -type f -o -type l \ fileList="$(find . -type f -o -type l \
| awk -v e="^./($excludeFiles)" '$0 !~ e { print $0 }')" | awk -v e="^./($excludeFiles)" '$0 !~ e { print $0 }')"
} }
listFiles() separateFileList()
{ {
# If unset [ -z "$fileList" ] && getFileList
[ -z "${files+x}" ] && setFiles
# Remove leading ./ from filepaths match="^./($systemDir)/"
echo "$files" | sed 's/^\.\///'
# Filter system directories and remove leading ./ from filepaths
homeFileList="$(echo "$fileList" \
| awk -v m="$match" '$0 !~ m { print substr($0, 3) }')"
# Filter non-system directories and remove leading ./ from filepaths
systemFileList="$(echo "$fileList" \
| awk -v m="$match" '$0 ~ m { print substr($0, 3) }')"
} }
add() filterFileList()
{ {
[ -z "$1" ] && return 1 if [ -z "$homeFileList" ] || [ -z "$systemFileList" ]; then
separateFileList
fi
# Filter on provided file name
homeFileListLoop="$(echo "$homeFileList" | grep "^$1")"
systemFileListLoop="$(echo "$systemFileList" | grep "^$1")"
}
fileAdd()
{
[ -z "$1" ] && exit 1
file="$(readlink -f "$(dirname "$1")")/$(basename "$1")" file="$(readlink -f "$(dirname "$1")")/$(basename "$1")"
fileCutHome="$(echo "$file" \ fileCutHome="$(echo "$file" \
@@ -108,52 +173,51 @@ add()
fi fi
} }
pullPush() filePull()
{ {
# If unset or empty string filterFileList "$1"
[ -z "$1" ] && return 1
# If unset for file in $homeFileListLoop; do
[ -z "${files+x}" ] && setFiles # /home/<user>/<file> -> dotfiles/<file>
cp -a "$HOME/$file" "$(pwd)/$file"
match="^./($systemDir)/"
# Filter system directories and remove leading ./ from filepaths
homeFiles="$(echo "$files" \
| awk -v m="$match" '$0 !~ m { print substr($0, 3) }')"
for f in $homeFiles; do
if [ "$1" = "pull" ]; then
# cp /home/<user>/<file> /[<some dir>/]dotfiles/<file>
cp -a "$HOME/$f" "$(pwd)/$f"
elif [ "$1" = "push" ]; then
mkdir -p "$(dirname "$HOME/$f")"
cp -a "$(pwd)/$f" "$HOME/$f"
fi
done done
# Filter non-system directories and remove leading ./ from filepaths for file in $systemFileListLoop; do
systemFiles="$(echo "$files" \ # /<file> -> dotfiles/<file>
| awk -v m="$match" '$0 ~ m { print substr($0, 3) }')" sudo cp -a "/$file" "$(pwd)/$file"
for f in $systemFiles; do
if [ "$1" = "pull" ]; then
# cp /<file> /[<some dir>/]dotfiles/<file>
sudo cp -a "/$f" "$(pwd)/$f"
elif [ "$1" = "push" ]; then
sudo cp -a "$(pwd)/$f" "/$f"
fi
done done
} }
pull() filePush()
{ {
pullPush "pull" filterFileList "$1"
for file in $homeFileListLoop; do
# dotfiles/<file> -> /home/<user>/<file>
mkdir -p "$(dirname "$HOME/$file")"
cp -a "$(pwd)/$file" "$HOME/$file"
done
for file in $systemFileListLoop; do
# dotfiles/<file> -> /<file>
sudo mkdir -p "$(dirname "/$file")"
sudo cp -a "$(pwd)/$file" "/$file"
done
} }
push() files()
{ {
pullPush "push" if [ "$1" = "list" ] || [ "$1" = "" ]; then
[ -z "$fileList" ] && getFileList
# Remove leading ./ from filepaths
echo "$fileList" | sed 's/^\.\///' | grep "$2"
elif [ "$1" = "add" ]; then
fileAdd "$2"
elif [ "$1" = "pull" ]; then
filePull "$2"
elif [ "$1" = "push" ]; then
filePush "$2"
fi
} }
# Packages # Packages
@@ -175,7 +239,7 @@ osDetect()
elif echo "$idLike" | grep -q 'ubuntu'; then elif echo "$idLike" | grep -q 'ubuntu'; then
os="debian" os="debian"
else else
echo "Unsupported operating system." >&2 echo "${b}${red}Error:${n} unsupported operating system." >&2
exit 1 exit 1
fi fi
} }
@@ -199,13 +263,13 @@ osDependencies()
binary="$(echo "$pair" | cut -d ':' -f 1)" binary="$(echo "$pair" | cut -d ':' -f 1)"
if ! command -v "$binary" > /dev/null; then if ! command -v "$binary" > /dev/null; then
dependency="$(echo "$pair" | cut -d ':' -f 2)" dependency="$(echo "$pair" | cut -d ':' -f 2)"
echo "Please install the '$dependency' dependency before running this option." >&2 echo "${b}${red}Error:${n} required dependency '$dependency' is missing." >&2
exit 1 exit 1
fi fi
done done
} }
packageList() getPackageList()
{ {
if [ "$os" = "arch" ]; then if [ "$os" = "arch" ]; then
filterList="$( (pacman -Qqg base base-devel; pactree -u base | tail -n +2) | sort)" filterList="$( (pacman -Qqg base base-devel; pactree -u base | tail -n +2) | sort)"
@@ -226,16 +290,15 @@ packageInstall()
# Grab everything off enabled official repositories that is in the list # Grab everything off enabled official repositories that is in the list
repoList="$(pacman -Ssq | grep -xf $packageFile)" repoList="$(pacman -Ssq | grep -xf $packageFile)"
if [ "$1" = "install" ]; then if [ "$1" = "aur-install" ]; then
# Install packages
echo "$repoList" | xargs --open-tty sudo pacman -Sy --needed
fi
if [ "$1" = "install-aur" ]; then
# Determine which packages in the list are from the AUR # Determine which packages in the list are from the AUR
aurList="$(grep -vx "$repoList" < $packageFile)" aurList="$(grep -vx "$repoList" < $packageFile)"
# Install AUR packages # Install AUR packages
echo "$aurList" | xargs --open-tty "$aurHelper" -Sy --needed --noconfirm echo "$aurList" | xargs --open-tty "$aurHelper" -Sy --needed --noconfirm
elif [ "$1" = "install" ]; then
# Install packages
echo "$repoList" | xargs --open-tty sudo pacman -Sy --needed
fi fi
elif [ "$os" = "debian" ]; then elif [ "$os" = "debian" ]; then
# Grab everything off enabled official repositories that is in the list # Grab everything off enabled official repositories that is in the list
@@ -252,60 +315,73 @@ packages()
if [ -z "$os" ]; then if [ -z "$os" ]; then
osDetect osDetect
osDependencies osDependencies
packageList
fi fi
if [ "$1" = "list" ] || [ "$1" = "" ]; then if [ "$1" = "list" ] || [ "$1" = "" ]; then
echo "$packageList" [ -z "$packageList" ] && getPackageList
echo "$packageList" | grep "$2"
elif [ "$1" = "store" ]; then elif [ "$1" = "store" ]; then
[ -z "$packageList" ] && getPackageList
echo "$packageList" > "$packageFile" echo "$packageList" > "$packageFile"
elif [ "$1" = "aur-install" ]; then
packageInstall "aur-install"
elif [ "$1" = "install" ]; then elif [ "$1" = "install" ]; then
packageInstall "install" packageInstall "install"
elif [ "$1" = "install-aur" ]; then
packageInstall "install-aur"
fi fi
} }
# Option handling # Option parsing
# -------------------------------------- # --------------------------------------
script="$(basename "$0")" script="$(basename "$0")"
options="$(getopt --options "ha:fp::ls" --longoptions "help,add:,files,packages::,pull,push" -n "$script" -- "$@" 2>&1)" parsed="$(getopt --options "hFPails" \
--longoptions "help,file,package,add,aur-install,install,pull,push,store" \
-n "$script" -- "$@" 2>&1)"
result="$?" result="$?"
# Exit if invalid option is provided # Exit if invalid option is provided
if [ "$result" -ne 0 ]; then if [ "$result" -ne 0 ]; then
echo "$options" | head -n 1 >&2 echo "$parsed" | head -n 1 >&2
echo "Try './$script --help' for more information." >&2 echo "Try './$script --help' for more information." >&2
exit 1 exit 1
fi fi
eval set -- "$options" eval set -- "$parsed"
while true; do while true; do
case "$1" in case "$1" in
-a | --add) -F | --file)
add "$2" if [ -n "$mode" ]; then
shift 2 echo "${b}${red}Error:${n} only one operation may be used at a time." >&2
;; exit 1
-f | --files) fi
listFiles mode="file"
shift shift
;; ;;
-h | --help) -P | --package)
help if [ -n "$mode" ]; then
exit echo "${b}${red}Error:${n} only one operation may be used at a time." >&2
exit 1
fi
mode="package"
shift
;; ;;
-p | --packages) -a | --add | --aur-install)
packages "$2" [ "$mode" = "file" ] && options="${options}add "
shift 2 [ "$mode" = "package" ] && options="${options}aur-install "
shift
;;
-i | --install)
options="${options}install "
shift
;; ;;
-l | --pull) -l | --pull)
pull options="${options}pull "
shift shift
;; ;;
-s | --push) -s | --push | --store)
push [ "$mode" = "file" ] && options="${options}push "
[ "$mode" = "package" ] && options="${options}store "
shift shift
;; ;;
--) --)
@@ -318,5 +394,58 @@ while true; do
esac esac
done done
# @Todo: # Target parsing
# push function to push just one file # --------------------------------------
targets="$*"
targetsNoHyphen="$(echo "$targets" | sed -E 's/(^-$|\s-|-\s)//g; s/(\s-\s)/ /;')"
# Read targets from stdin
if [ "$targets" != "$targetsNoHyphen" ]; then
if [ -t 0 ]; then
echo "${b}${red}Error:${n} argument '-' specified without input on stdin." >&2
exit 1
fi
eval set -- "$targetsNoHyphen $(cat /dev/stdin)"
fi
# Execute
# --------------------------------------
if [ -z "$mode" ]; then
echo "${b}${red}Error:${n} no operation specified (use -h for help)" >&2
exit 1
fi
if [ "$mode" = "file" ]; then
if [ -z "$options" ]; then
files "list" "$@"
exit
fi
for option in $options; do
if [ -z "$*" ]; then
if [ "$option" = "add" ];then
echo "${b}${red}Error:${n} no files or directories selected to add." >&2
exit 1
fi
files "$option"
continue
fi
for path; do
files "$option" "$path"
done
done
fi
if [ "$mode" = "package" ]; then
if [ -z "$options" ]; then
packages "list" "$@"
exit
fi
for option in $options; do
packages "$option"
done
fi