#!/bin/zsh
setopt no_unset warn_create_global no_multibyte
zmodload zsh/stat
typeset -gA ftypes hardlinks s
typeset -g delim find fname ftype fmode
ftypes=( # convert hex type to mnemonic character
C s # socket
c s
A l # symbolic link
a l
8 f # regular file
6 b # block device
4 d # directory
2 u # character device
1 p # FIFO
)
### Defaults for options passed by env {{{1
# TODO: make into command-line arguments
: ${compact:=1}
: ${print_m:=1}
: ${print_o:=1}
: ${print_c:=1}
: ${print_s:=1}
if (($+commands[file])) && (($+commands[base64])); then
: ${print_b:=1}
else
: ${print_b:=0}
if (($+commands[file])) && (($+commands[xxd])); then
: ${print_x:=1}
fi
fi
: ${print_x:=0}
: ${max_bin_size:=}
: ${max_newline_size:=1024}
### Generic helpers {{{1
die() {
printf '%s\n' "$@"
exit 1
}
### FileSet writer functions {{{1
statement() {
# start on new line for multiline statements - more readable
if [[ -n "$delim" && $1 == *$'\n'* ]]; then
delim=$'\n'
fi
printf '%s%s' $delim ${1//$'\n'/$'\n\t'}
delim=${2:-$'\t'}
}
statement_end() {
printf '\n'
delim=''
}
process_file() {
local -a find_info
local t fmode owner size filename
find_info=( $=1 )
t=$find_info[1]
fmode=$find_info[2]
owner=$find_info[3]
size=$find_info[4]
# --- print stuff ---
(($compact)) || printf '\n'
filename="$fname"
if [[ $filename == . ]]; then
filename=/
elif [[ $filename == ./* ]]; then
filename=$filename[2,-1]
fi
filename=${filename#/}
# printf "%s %s %s\n" >&2 $t $fmode "${(qqq)filename}"
if [[ $filename == *$'\t'* || $filename == *$'\n'* ]]; then
statement $'P\t'$filename $'\t'
else
if (($compact)); then
statement /${filename}
else
statement /${filename} $'\n'
fi
fi
# Note: not supporting hardlinks (yet)
if [[ $t == [bu] ]]; then
zstat -LH s $fname || die "stat failed on ${(qqq)fname}"
statement $t$(( $s[rdev] >> 8 )):$(( $s[rdev] & 255 ))
elif [[ $t == l ]]; then
zstat -LH s $fname || die "stat failed on ${(qqq)fname}"
statement $'l\t'$s[link] $'\t'
elif [[ $t == f ]]; then
if (($print_c)); then
process_file_data $size
else
statement f
if (($print_s)); then
statement s$'\tSHA512:'${"$(sha512sum <$fname)"%% *} \
|| die "Could not read ${(qqq)fname}"
fi
fi
else
statement $t
fi
(($print_o)) && statement o$owner
(($print_m)) && statement m$fmode
statement_end
}
process_file_data() {
local size
size=$1
if ! (($size)); then
statement cN$'\t' # empty file
return
fi
# print binary representation?
if (($print_b | $print_x)) && \
[[ $(file -bi "$fname") != text/* ]]
then
if (($size > 256)); then
if [[ -n $max_bin_size && $size -gt $max_bin_size ]]; then
statement s$'\tSHA512:'${"$(sha512sum <$fname)"%% *} \
|| die "Could not read ${(qqq)fname}"
elif (($print_x)); then
if [[ -n "$delim" ]]; then
printf '\nX'
else
printf 'X'
fi
xxd <$fname | sed 's/^/\t/'
((${(j.|.)pipestatus})) && die "Could not read ${(qqq)fname}"
statement_end
else
if [[ -n "$delim" ]]; then
printf '\nB'
else
printf 'B'
fi
base64 <$fname | sed 's/^/\t/'
((${(j.|.)pipestatus})) && die "Could not read ${(qqq)fname}"
statement_end
fi
else
if [[ -n $max_bin_size && $size -gt $max_bin_size ]]; then
statement s$'\tSHA512:'${"$(sha512sum <$fname)"%% *}
elif (($print_x)); then
statement X$'\t'"$(xxd <$fname)" $'\n'
else
statement B$'\t'"$(base64 <$fname)" $'\n'
fi
fi
return
fi
# print text
# if file is longer than this, always use CN
if (($size > $max_newline_size)); then
if [[ -n "$delim" ]]; then
printf '\nCN\t'
else
printf 'CN\t'
fi
# Swap NL with @ so trailing newline is handled correctly
tr <$fname '\n@' '@\n' \
| sed 's/@/@\t/g' \
| tr '@\n' '\n@'
((${(j.|.)pipestatus})) && die "Could not read ${(qqq)fname}"
statement_end
return
fi
# read and then print out, determinig trailing newline flags
local content flags
content="$(<$fname)"
flags=''
if [[ $content == *$'\n' ]]; then
content=${content%$'\n'}
if [[ $content == *$'\n' ]]; then
# force appending newline
flags+=n
fi
else
flags+=N
fi
if ! (($compact)) || [[ $content == *$'\t'* || $content == *$'\n'* ]]; then
statement C$flags$'\t'$content $'\n'
else
statement c$flags$'\t'$content
fi
}
### Mainloop {{{1
if (($+ROOT)) && [[ -n $ROOT ]]; then
cd $ROOT || exit $?
fi
find "$@" -printf '%y %m %U:%G %s\t%p\0' \
| sort -z -t $'\t' -k 2 \
| while IFS=$'\t' read -r -d $'\0' find fname
do
process_file "$find" "$fname"
done