#!/bin/bash
#
# ctct - a simple console contact manager
#
# Copyright 2015, 2016 Einhard Leichtfuß
#
# ctct is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ctct is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with ctct. If not, see .
#
## DEFAULT SETTINGS:
system_config_dir="@default_confdir@"
user_config_dir="@default_user_config_dir@"
datadir="@default_datadir@"
fallback_editor="@default_fallback_editor@"
default_editor= # none - use $EDITOR
input_program="@default_input_program@"
output_program="@default_output_program@"
visual_program="@default_visual_program@"
confirm_deletion=@default_confirm_deletion@
confirm_default_yes=@default_confirm_default_yes@
## USER SETTINGS:
test -f "$system_config_dir/config.sh" \
&& source "$system_config_dir/config.sh"
test -f "$user_config_dir/config.sh" \
&& source "$user_config_dir/config.sh"
## CONSTANTS:
exec_name="${0##*/}"
TRUE=0
FALSE=1
function print_help() {
cat << EOF
usage:
$exec_name - show contact
$exec_name -l - list all entries
$exec_name [-s] [...] - search by name
$exec_name -S [...] - search by data
$exec_name -e - edit / create entry
$exec_name -d [...] - delete entries
$exec_name --rename - rename entry
$exec_name --version - show version information
$exec_name -h - print this help
EOF
}
function main()
{
if ! test -d "$datadir" && ! mkdir "$datadir"
then
print_error "$exec_name: $datadir could not be created, quitting..."
exit 1
fi
test $# -eq 0 && print_help && exit 1
if [ "$1" = "-h" ] || ( [[ "--help" =~ ^"$1" ]] && [ ${#1} -ge 3 ] )
then
print_help; exit 0
elif [[ "--version" =~ "$1" ]] && [ ${#1} -ge 3 ]
then
print_version; exit 0
elif [ "$1" = "-l" ] || \
( [[ "--list-all" =~ ^"$1" ]] && [ ${#1} -ge 3 ] )
then
list_all; exit $?
elif [ "$1" = "-s" ] || \
( [[ "--search-by-name" =~ ^"$1" ]] && [ ${#1} -ge 13 ] )
then
shift; find_similar "Found:" "$@"; exit $?
elif [ "$1" = "-S" ] || \
( [[ "--search-by-data" =~ ^"$1" ]] && [ ${#1} -ge 13 ] )
then
shift; search_file "Found:" "$@"; exit $?
elif [ "$1" = "-e" ] || ( [[ "--edit" =~ ^"$1" ]] && [ ${#1} -ge 3 ] )
then
test $# -lt 2 && print_error "$exec_name: no entry specified." \
&& exit 1
edit_file "$2"; exit $?
elif [ "$1" = "-d" ] || \
( [[ "--delete" =~ ^"$1" ]] && [ ${#1} -ge 3 ] )
then
test $# -lt 2 && print_error "$exec_name: no entry to be deleted." \
&& exit 1
shift; delete_file "$@"; exit $?
elif [[ "--rename" =~ ^"$1" ]] && [ ${#1} -ge 3 ]
then
( test $# -lt 2 && print_error "$exec_name: no entry specified." ) || \
( test $# -lt 3 && print_error "$exec_name: no new name specified." ) \
&& exit 1
rename_file "$2" "$3"; exit $?
elif [[ "$1" =~ ^- ]]; then
print_error "$exec_name: unknown option '$1'"; exit 1
fi
if ! find_exact "$1"
then
find_similar "Did you mean:" "$@" || print_msg "No match found."
fi
}
function print_msg()
{
printf "%s\n" "$@"
}
function print_error()
{
printf "%s\n" "$@" >&2
}
function find_exact()
{
local file
file="$datadir/$(get_filename "$1")" || return $FALSE
if test "$visual_program" = "cat"
then
"$output_program" < "$file"
else
eval "$output_program" < "$file" | "$visual_program"
fi
return $TRUE
}
# $1: initial success message; $2-${$#}: search-patterns
function find_similar()
{
local found=false msg="$1" name bool file pattern
shift 1 # skip $1
# disallow '.' in any pattern
# - any pattern should be either part of the first or the last name
# else the reverse order (last.first) would have to be checked as well
if [[ "$*" =~ \. ]]
then
return $FALSE
fi
for file in "$datadir"/*
do
name="${file##*/}"
bool=true
for pattern in "${@,,}"
do
if ! [[ "${name,,}" =~ "$pattern" ]]
then
bool=false
break
fi
done
if $bool
then
! $found && print_msg "$msg" && found=true
print_msg " $name"
fi
done
$found && return $TRUE || return $FALSE
}
function search_file()
{
local found=false msg="$1" valid file pattern
shift 1 # skip $1
for file in "$datadir"/*
do
valid=true
if test "$output_program" = "cat"
then
for pattern in "$@"
do
! grep -qi "$pattern" "$file" && valid=false && break
done
else
for pattern in "$@"
do
! eval "$output_program" < "$file" | \
grep -qi "$pattern" "$file" && valid=false && break
done
fi
if $valid
then
! $found && print_msg "$msg" && found=true
print_msg " ${file##*/}"
fi
done
$found && return $TRUE || return $FALSE
}
function list_all()
{
ls -1 "$datadir"
}
function edit_file()
{
local file tmp_file editor new=false
if ! file="$datadir/$(get_filename "$1")"
then
check_syntax "$1" && file="$datadir/$1" && new=true || return $FALSE
fi
if test -n "$default_editor"
then
editor="$default_editor"
elif test -n "$EDITOR" && which "$EDITOR" >/dev/null 2>&1
then
editor="$EDITOR"
else
editor="$fallback_editor"
fi
if test "$input_program" = "cat" -a "$output_program" = "cat"
then
$new && touch "$file" # vim does not save an empty file
"$editor" "$file"
else
tmp_file="$(mktemp)"
# output_program may contain further arguments:
! $new && eval "$output_program" < "$file" > "$tmp_file"
"$editor" "$tmp_file"
eval "$input_program" < "$tmp_file" > "$file"
fi
}
function rename_file()
{
check_syntax "$2" || return $FALSE
local file
if file="$datadir/$(get_filename "$1")"
then
if get_filename "$2" > /dev/null
then
print_error "$exec_name: Entry \"$2\" does already exist."
else
mv "$file" "$datadir/$2"
fi
else
print_error "$exec_name: Entry \"$1\" does not exist."
return 1
fi
}
function delete_file()
{
local i files name file str
typeset -i i=0
typeset -a files
# check for existance of all to be deleted files
for name in "$@"
do
if file="$(get_filename "$name")"
then
files[i++]="$file"
else
print_error "$exec_name: Contact \"$file\" does not exist, aborting..."
return 1
fi
done
if "$confirm_deletion"; then
# prepare confirmation
if test $# -eq 1; then
str="Do you really want to delete the entry ${files[0]}"
else
print_msg "Entries to be deleted:"
for file in "${files[@]}"; do
print_msg " $file"
done
str="Are you sure to delete all these entries"
fi
# confirm and delete
if confirm "$str"; then
for file in "${files[@]}"; do
rm "$datadir/$file"
done
fi
else # simply delete
for file in "${files[@]}"; do
rm "$datadir/$file"
done
fi
}
function get_filename()
{
local rev
if test -f "$datadir/$1"; then
printf "%s\n" "$1"
elif [[ "$1" =~ \. ]]
then
rev=( ${1/\./ } )
rev="${rev[1]}.${rev[0]}"
if test -f "$datadir/$rev"; then
printf "%s\n" "$rev"
else
return $FALSE
fi
else
return $FALSE
fi
}
# $1: confirmation string
function confirm()
{
local str="$1 " var
if $confirm_default_yes; then
str+="[Y/n]? "
else
str+="[y/N]? "
fi
printf "%s" "$str"
read var
if test -z "$var"; then
$confirm_default_yes && return $TRUE || return $FALSE
fi
var="${var,,}"
if test "$var" = "y" -o "$var" = "yes"; then
return $TRUE
else
return $FALSE
fi
}
function check_syntax()
{
if [[ "$1" =~ ^[A-Za-z]+([-_][A-Za-z]+)*(\.[A-Za-z]+([-_][A-Za-z]+)*)?$ ]]
then
return $TRUE
else
print_error "$exec_name: invalid name \"$1\"" \
" An entry name may only contain letters," \
" '-' and '_' as separators and exactly one dot ('.')"
return $FALSE
fi
}
function print_version() {
cat << EOF
ctct - Version @PACKAGE_VERSION@
Copyright 2015, 2016 Einhard Leichtfuß.
License AGPLv3+: GNU Affero General Public License, version 3 or later
-
This is free software: You are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
EOF
}
main "$@"
# vim: ts=2 sw=2