#! /bin/bash
#
# ff - simple file search utility
# Copyright (c) 2017, 2022, 2025  Paulina Laura Emilia <polyna@posteo.net>
#
# This program 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.
#
# This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
#
all=0
follow=
format=print
ignorecase=
match=name
prog=${0##/*/}
type=()
xdev=0

function usage {
	printf "Try ‘%s --help’ for more information.\n" "$prog" >&2
	exit 1
}

opt=$(getopt -T)
if [[ $? -eq 4 && -z $opt ]]; then
	opt=$(getopt -n "$prog" \
		-o 0abcdfHilLpPsvw \
		-l all \
		-l block-special \
		-l character-special \
		-l directory \
		-l file \
		-l follow \
		-l follow-command-line \
		-l help \
		-l ignore-case \
		-l link \
		-l no-follow \
		-l no-ignore-case \
		-l null \
		-l one-file-system \
		-l pipe \
		-l socket \
		-l verbose \
		-l version \
		-l whole-path \
		-- "$@") || usage
	eval "set -- $opt"
fi

while :; do
	option=$1
	shift

	case $option in
	-a|--all)
		all=1 ;;
	-H|--follow-command-line)
		follow=H ;;
	-L|--follow)
		follow=L ;;
	-P|--no-follow)
		follow= ;;
	-v|--verbose)
		format=ls ;;
	-0|--null)
		format=print0 ;;
	--no-ignore-case)
		ignorecase=0 ;;
	-i|--ignore-case)
		ignorecase=1 ;;
	-w|--whole-path)
		match=path ;;
	-b|--block-special)
		type+=(b) ;;
	-c|--character-special)
		type+=(c) ;;
	-d|--directory)
		type+=(d) ;;
	-f|--file)
		type+=(f) ;;
	-l|--link)
		type+=(l) ;;
	-p|--pipe)
		type+=(p) ;;
	-s|--socket)
		type+=(s) ;;
	--one-file-system)
		xdev=1 ;;
	--help)
		printf "Usage: %s [OPTION]... STRING [DIRECTORY]...
Searches for files and directories whose names contain STRING in the specified
DIRECTORY (or directories) and all its subdirectories.

If STRING contains ‘*’, ‘?’ or ‘[’, it is treated as a wildcard pattern and
matching file and directory names are returned. Wildcard characters may need
to be escaped to prevent them from being expanded by the shell.

If --no-ignore-case is not specified and STRING contains neither uppercase
letters nor wildcards, case-insensitive searching is assumed.

If DIRECTORY is omitted, the current working directory is searched.

Mandatory arguments for long options are also mandatory for short options.

      --help                 display this help and exit
      --version              output version information and exit

Name matching:
  -a, --all                  do not exclude hidden files and directories
  -i, --ignore-case          ignore case distinctions
      --no-ignore-case       do not ignore case distinctions
  -w, --whole-path           match the whole path name relative to the
                               DIRECTORY where the file or directory was found

Type matching:
  -b, --block-special        match only block (buffered) special files
  -c, --character-special    match only character (unbuffered) special files
  -d, --directory            match only directories
  -f, --file                 match only regular files
  -l, --link                 match only symbolic links
  -p, --pipe                 match only pipes
  -s, --socket               match only sockets

Multiple options can be specified to match files and directories of different
types.

File system traversal:
  -L, --follow               follow symbolic links where possible
  -H, --follow-command-line  follow symbolic links listed on the command line
  -P, --no-follow            do not follow symbolic links (default)
      --one-file-system      do not descend directories on other file systems

Output format:
  -v, --verbose              print detailed information about the matches
  -0, --null                 print matches separated by a null character,
                               not a line break.

ff home page: <https://polyna.eu/computing/>\n" "$prog"
		exit ;;
	--version)
		printf 'ff 2025.04.29

Copyright (C) 2025 Paulina Laura Emilia
License AGPLv3+: GNU AGPL version 3 or later <https://gnu.org/licenses/agpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.\n'
		exit ;;
	-[!-]?*)
		set -- "${option:0:2}" "-${option:2}" "$@" ;;
	--)
		break ;;
	-*)
		printf "%s: unrecognized option '%s'\n" "$prog" "$option" >&2
		usage ;;
	*)
		set -- "$option" "$@"
		break
	esac
done

if (( !$# )); then
	printf '%s: missing operand\n' "$prog" >&2
	usage
fi

string=$1
shift

# If a string doesn't contain any wildcard character, match it as a substring
# of a file name. If it also doesn't contain any uppercase characters, match
# it case-insensitively.
if [[ $string != [*?\[\]]* && $string != *[!\\][*?\[\]]* ]]; then
	[[ -z $ignorecase && $string != *[[:upper:]]* ]] &&
		ignorecase=1

	string=\*$string\*
fi

(( ignorecase )) &&
	match=i$match

# Build argument list.
arg=()

# Follow symbolic links if requested.
[[ -n $follow ]] &&
	arg+=("-$follow")

# Add starting point paths.
if (( $# )); then
	arg+=("${@/#-/.\/-}")
else
	arg+=(.)
fi

# Add file system traversal operands.
(( xdev )) &&
	arg+=(-xdev)

# Prune hidden files unless all results are requested.
(( !all )) &&
	arg+=(-name .\* ! -name . ! -name .. -prune -o)

# Add file type search operands.
case ${#type[@]} in
0)	;;
1)	arg+=(-type "${type[0]}") ;;
*)	arg+=(\()
	for t in "${type[@]}"; do
		arg+=(-type "$t" -o)
	done
	arg[-1]=\)
esac

exec -a "$prog" find "${arg[@]}" "-$match" "$string" "-$format"
