~q3cpma/mtg-tools

ebe3f971e497f6b31ee378ed87f77b5aa8d56754 — q3cpma 1 year, 9 months ago 8424409
Modernize scripts and update lists
M README => README +9 -7
@@ 8,17 8,15 @@
* mana_curve_plot.sh:  same as above, but produces a nice plot instead of a
                       CSV.

You'll need to get the cards yourself using the torrent at [1] and use
`extract_pictures.sh` before using some of these.

[1] http://www.slightlymagic.net/wiki/Magic_Album
Most of these require the MTG_IMGDIR environment variable set to where you put
the Magic picture album that you downloaded (and extracted) from [1].


    Dependencies and portability
    ============================

* POSIX environment
* GNU parallel (`extract_pictures.sh` and `list_collage.sh`)
* GNU parallel (`list_collage.sh`)
* unzip (`extract_pictures.sh`)
* sxiv (`find_card.sh`, can easily be changed)
* imagemagick 7 (`list_collage.sh`)


@@ 27,8 25,8 @@ You'll need to get the cards yourself using the torrent at [1] and use
Portability requirements are detailed in each script. To resume it, the base
portability is GNU, *BSD, MacOS and Illumos, but `list_collage.sh` needs
`sort -V` which is not present on NetBSD, MacOS nor Illumos. Fortunately, it
can easily be replaced by other natural sorting tools (I have a simple and fast
one called natsort here: https://repo.or.cz/misc-tools.git).
can easily be replaced by other natural sorting tools (a simple and fast one
can be had at [2]).


    List format


@@ 89,3 87,7 @@ Some remarks about the format:
| #Lands                            |
| 20  Plains [4]      (Odyssey)     |
+-----------------------------------+


[1] http://www.slightlymagic.net/wiki/Magic_Album
[2] https://git.sr.ht/~q3cpma/misc-tools
\ No newline at end of file

M bin/extract_pictures.sh => bin/extract_pictures.sh +28 -11
@@ 1,23 1,40 @@
#!/bin/sh
# Dependencies: GNU parallel, unzip
# Portability:  GNU, *BSD, MacOS, Illumos, HP-UX
# +--------------+-----+---------+--------+--------------+---------+-------+---------+-------+-----+------+-------+
# | cmd/OS       | GNU | OpenBSD | NetBSD | DragonflyBSD | FreeBSD | MacOS | Illumos | HP-UX | AIX | IRIX | Tru64 |
# +--------------+-----+---------+--------+--------------+---------+-------+---------+-------+-----+------+-------+
# | find -exec + | o   | o       | o      | o            | o       | o     | o       | o     |     |      |       |
# +--------------+-----+---------+--------+--------------+---------+-------+---------+-------+-----+------+-------+
# Dependencies: unzip
# Portability:  POSIX
set -eu
. "$(dirname -- "$0")"/util.sh
. "$(cd -P -- "$(dirname -- "$0")"; pwd -P)"/util.sh


imgdir=${MTG_IMGDIR:-$HOME/Data/Games/MTG/pictures}
requirefile -d "$imgdir"
check_imgdir "$imgdir"

if [ $# -ne 0 ] || is_help "$@"
then
	cat <<EOF | text_format
**NAME**
    $(basename -- "$0") - Extract the slightlymagic album zips

**SYNOPSIS**
    $(basename -- "$0") [**-h**]

**ENVIRONMENT**
    MTG_IMGDIR
        Where the magic album is located. Defaults to
        \$HOME/Data/Games/MTG/pictures

EOF
	exit 1
fi


# Some zip files can fail
set +e
find "$imgdir" -type f -path "$imgdir/Special Sets/Renaissance/FRA.zip" -o \
find "$imgdir" \( -type f -path "$imgdir/Special Sets/Renaissance/FRA.zip" -o \
	-name 'ENG NTR.zip' -o -name 'ENG REP.zip' -o -name 'ENG FOIL.zip' -o \
	-name 'ENG.zip' | parallel --no-notice --eta unzip -qu {} -d {//}
	-name 'ENG.zip' \) | while IFS= read -r zip
do
	unzip -qu "$zip" -d "${zip%/*}"
done
set -e

find "$imgdir" -type f -name '*.zip' -exec rm -- {} +

M bin/find_card.sh => bin/find_card.sh +9 -5
@@ 2,23 2,27 @@
# Dependencies: sxiv
# Portability:  POSIX
set -eu
. "$(dirname -- "$0")"/util.sh
selfdir=$(cd -P -- "$(dirname -- "$0")"; pwd -P)
. "$selfdir"/util.sh
. "$selfdir"/mtg_common.sh


imgdir=${MTG_IMGDIR:-$HOME/Data/Games/MTG/pictures}
check_imgdir "$imgdir"

if [ $# -lt 1 ] || [ $# -gt 2 ] || is_help "$@"
then
	name=$(basename "$(readlinkf "$0")")
	cat <<EOF | text_format
**NAME**
    $(basename -- "$0") - Search for a MTG card and display the results
    $name - Search for a MTG card and display the results

**SYNOPSIS**
    $(basename -- "$0") [**-h**] __card_name_ERE__ [__set_name_ERE__]
    $name [**-h**] __card_name_ERE__ [__set_name_ERE__]

**ENVIRONMENT**
    MTG_IMGDIR
        Where the card pictures are located. Defaults to
        Where the magic album is located. Defaults to
        \$HOME/Data/Games/MTG/pictures

EOF


@@ 26,5 30,5 @@ EOF
fi

find "$imgdir" -type f -name '*.jpg' | grep -Eix \
	".*/${2:+[^/]*$2[^/]*/}[^/]*$(echop "$1" | sed 's#//#_#')[^/]*" | \
	".*/${2:+[^/]*$2[^/]*/}[^/]*$(pecho "$1" | sed 's#//#_#')[^/]*" | \
	sxiv -N floating -g 480x698 -

M bin/list_collage.sh => bin/list_collage.sh +20 -20
@@ 9,19 9,20 @@
# | sort -V   | o   | o       |        | o            | o       |       |         |       |     |      |       |
# +-----------+-----+---------+--------+--------------+---------+-------+---------+-------+-----+------+-------+
set -eu
_dir=$(dirname -- "$0")
. "$_dir"/util.sh
. "$_dir"/mtg_common.sh
selfdir=$(cd -P -- "$(dirname -- "$0")"; pwd -P)
. "$selfdir"/util.sh
. "$selfdir"/mtg_common.sh


if [ $# -lt 1 ] || [ $# -gt 2 ] || is_help "$@"
then
	name=$(basename "$(readlinkf "$0")")
	cat <<EOF | text_format
**NAME**
    $(basename -- "$0") - Create a MTG list collage
    $name - Create a MTG list collage

**SYNOPSIS**
    $(basename -- "$0") [**-h**] __LIST__ [__OUT__]
    $name [**-h**] __LIST__ [__OUT__]

**DESCRIPTION**
    Read __LIST__ according to format.adoc and create a collage. The collage


@@ 29,16 30,16 @@ then

**ENVIRONMENT**
    MTG_IMGDIR
        Where the card pictures are located. Defaults to
        Where the magic album is located. Defaults to
        \$HOME/Data/Games/MTG/pictures

EOF
	exit 1
fi
requirefile -f "$1"

check_imgdir "${MTG_IMGDIR:-$HOME/Data/Games/MTG/pictures}"
workdir=$(mktemp -d)
tempscript=$(mktemp)
tempscript=$(pmktemp)
out=${2:-${1%.*}.jpg}

trap 'rm -r -- "$workdir"; rm -f -- "$tempscript"; exit' INT TERM HUP QUIT EXIT


@@ 50,8 51,8 @@ cat <<-'EOF' >"$tempscript"
	. "$3"/mtg_common.sh
	imgdir=${MTG_IMGDIR:-$HOME/Data/Games/MTG/pictures}
    line_parse "$1"
	file=$(find "$imgdir" -type f -name '*.jpg' | grep -iF \
		"$(ter "[ ! -z \"$set\" ]" "/${set}/${name}." "/${name}.")" | tail -n1)
	file=$(find "$imgdir" -type f -name '*.jpg' |
		grep -iF "${set:+/$set}/${name}." | tail -n1)
	if [ ! -f "$file" ]
	then
		printf '"%s (%s)": card not found\n' "$name" "$set" >&2


@@ 75,22 76,21 @@ do
    subwdir=$(mktemp -d "$workdir/${i}_XXX")
	subout=$(mktemp "$workdir/${cnt}_${i}_XXX.png")

	list=$(tolower <"$1" | "$_dir"/pat_extract.sh "^#$i\$" '^$' | list_parse)
	list=$(tolower <"$1" | "$selfdir"/pat_extract.sh "^#$i\$" '^$' | list_parse)
	if [ "$list" ]
	then
		listlen=$(echop "$list" | wc -l)
		echop "$i ($cnt/7)... ($listlen)"
		listlen=$(pecho "$list" | wc -l)
		pecho "$i ($cnt/7)... ($listlen)"
		set +e
		echop "$list" | sed 's#//#_#' | parallel --halt now,fail=1 \
			"$tempscript" {} "$subwdir/{#}.jpg" "$_dir"
		pecho "$list" | sed 's#//#_#' | parallel --halt now,fail=1 \
			"$tempscript" {} "$subwdir/{#}.jpg" "$selfdir"
		# Trigger trap
		[ $? -eq 1 ] && kill $$
		set -e

		find -- "$subwdir" -type f | sort -V |
			"$_dir"/rename.sh '$dir/$count.$ext'
		find -- "$subwdir" -type f | sort -V | "$selfdir"/rename.sh '$dir/$count.$ext'
		# 11 is pretty good on a 16:9 monitor
		tile=$(ter "[ \"$listlen\" -gt 11 ]" '11x' "${listlen}x")
		tile=$((listlen > 11 ? 11 : listlen))x
		magick montage -geometry +0+0 -tile $tile "$subwdir"/* "$subout"
	else
		rm -- "$subout"


@@ 100,5 100,5 @@ done

magick montage -geometry +0+0 -tile 1x "$workdir"/*.png miff:- | \
	magick convert - -undercolor '#000000a0' -fill white -gravity southeast \
	-pointsize 60 -quality 97 -annotate +0+0 \
	"total: $("$_dir"/list_size.sh "$1") cards" "$out"
	-pointsize 60 -quality 98 -annotate +0+0 \
	"total: $("$selfdir"/list_size.sh "$1") cards" "$out"

M bin/list_size.sh => bin/list_size.sh +6 -4
@@ 2,18 2,20 @@
# Dependencies:
# Portability:  POSIX
set -eu
. "$(dirname -- "$0")"/util.sh
. "$(dirname -- "$0")"/mtg_common.sh
selfdir=$(cd -P -- "$(dirname -- "$0")"; pwd -P)
. "$selfdir"/util.sh
. "$selfdir"/mtg_common.sh


if [ $# -gt 1 ] || is_help "$@"
then
	name=$(basename "$(readlinkf "$0")")
	cat <<EOF | text_format
**NAME**
    $(basename -- "$0") - Print the card number of an MTG list
    $name - Print the card number of an MTG list

**SYNOPSIS**
    $(basename -- "$0") [__LIST__]
    $name [__LIST__]

**DESCRIPTION**
    if __LIST__ isn't set, stdin is read as an MTG list.

M bin/mana_curve.sh => bin/mana_curve.sh +17 -11
@@ 2,26 2,32 @@
# Dependencies:
# Portability:  POSIX
set -eu
. "$(dirname -- "$0")"/util.sh
. "$(dirname -- "$0")"/mtg_common.sh
selfdir=$(cd -P -- "$(dirname -- "$0")"; pwd -P)
. "$selfdir"/util.sh
. "$selfdir"/mtg_common.sh


if [ $# -ne 1 ] || is_help "$@"
then
	name=$(basename "$(readlinkf "$0")")
	cat <<EOF | text_format
**NAME**
    $(basename -- "$0") - Create the mana curve CSV for an MTG list
    $name - Create the mana curve CSV for an MTG list

**SYNOPSIS**
    $(basename -- "$0") __LIST__
    $name __LIST__
EOF
	exit 1
fi

printf '#CMC,Card count\n'
grep -- '^#CMC' "$1" | sort -n | uniq | cut -c5- | while IFS= read -r cmc
do
	printf '%d,' $cmc
	pat_extract.sh "#CMC$cmc" '^(#|$)' <"$1" | list_parse | cut -f1 | \
		paste -sd+ | bc
done
out=$(
	grep -- '^#CMC' "$1" | sort -n | uniq | cut -c5- | while IFS= read -r cmc
	do
		printf '%d,' $cmc
		pat_extract.sh "#CMC$cmc" '^(#|$)' <"$1" | list_parse | cut -f1 | \
			paste -sd+ | bc
	done
)
printf '#CMC,Card count\n#Average,%.2f\n%s\n' \
	"$(echo "$out" | awk -F, '{sum += $1 * $2; num += $2} END {print sum / num}')" \
	"$out"

M bin/mana_curve_plot.sh => bin/mana_curve_plot.sh +20 -24
@@ 1,53 1,49 @@
#!/bin/sh
# Dependencies: gnuplot
# Portability:  GNU, *BSD, MacOS, Illumos, HP-UX, Tru64
# +-----------+-----+---------+--------+--------------+---------+-------+---------+-------+-----+------+-------+
# | cmd/OS    | GNU | OpenBSD | NetBSD | DragonflyBSD | FreeBSD | MacOS | Illumos | HP-UX | AIX | IRIX | Tru64 |
# +-----------+-----+---------+--------+--------------+---------+-------+---------+-------+-----+------+-------+
# | mktemp    | o   | o       | o      | o            | o       | o     | o       | o     |     |      | o     |
# +-----------+-----+---------+--------+--------------+---------+-------+---------+-------+-----+------+-------+
# Portability:  POSIX
set -eu
_dir=$(dirname -- "$0")
. "$_dir"/util.sh
selfdir=$(cd -P -- "$(dirname -- "$0")"; pwd -P)
. "$selfdir"/util.sh


if [ $# -lt 1 ] || [ $# -gt 2 ] || is_help "$@"
then
	name=$(basename "$(readlinkf "$0")")
	cat <<EOF
**NAME**
    $(basename -- "$0") - Create the mana curve plot for an MTG list
    $name - Create the mana curve plot for an MTG list

**SYNOPSIS**
    $(basename -- "$0") [**-h**] __LIST__ [__WIDTHxHEIGHT__]
    $name [**-h**] __LIST__ [__WIDTHxHEIGHT__]

EOF
	exit 1
fi
requirefile -f "$1"
[ $# -eq 2 ] && ! match '[0-9]+x[0-9]+' "$2" && die "$2: bad dimensions"

outw=640
outh=480
if [ $# -eq 2 ]
then
	outw=${2%x*}
	outh=${2#*x}
fi
[ $# -eq 2 ] && outdim="${2%x*} ${2#*x}" || outdim='640 480'
out=${2:-${1%.*}.mc.png}
temp=$(mktemp)

data=$("$selfdir"/mana_curve.sh "$1")
comment=$(pecho "$data" | sed -n '2{s/^#//; s/,/: /; p}')
IFS=, read_split "$(pecho "$data" | sed 's/^#//p; q')" xlabel ylabel

temp=$(pmktemp)
trap 'exit 1' INT TERM HUP QUIT
trap 'rm -- "$temp"' EXIT
"$selfdir"/mana_curve.sh "$1" | grep -v '^#' > "$temp"


"$_dir"/mana_curve.sh "$1" | tail -n+2 > "$temp"
gnuplot <<- EOF
	set datafile separator ","
	set terminal png size $outw $outh
	set terminal png size $outdim
	set output "$out"
	set title "${1%.*} - Mana Curve"
	#set xlabel "CMC"
	#set ylabel "Card count"
	set obj 10 rect at graph 0.85, graph 0.95 size char strlen("$comment"), char 1
	set obj 10 fillstyle empty border 0 front
 	set label 10 at graph 0.85, graph 0.95 "$comment" front center
	set xlabel "$xlabel"
	set ylabel "$ylabel"
	# set offset 0, 1, 1, 0
	set xrange[0:]
	set yrange[0:]
	set xtics 1

M bin/mtg_common.sh => bin/mtg_common.sh +17 -2
@@ 18,7 18,22 @@ list_parse()

line_parse()
{
	IFS=$(printf '\t') read -r count name set <<-EOF
		$(printf '%s\n' "$1")
	IFS=$(printf '\t') read_split "$1" count name set
}

check_imgdir()
{
	requirefile -d "$1"
	_expected_content=$(cat <<-EOF
		Back.jpg
		Core Sets
		Expansions
		Promo Cards
		Special Sets
	EOF
	)
	if [ "$(ls "$1" | sort)" != "$_expected_content" ]
	then
		die "$imgdir: invalid image directory, must contain $_expected_content"
	fi
}

M bin/pat_extract.sh => bin/pat_extract.sh +11 -17
@@ 2,17 2,19 @@
# Dependencies: util.sh
# Portability:  POSIX
set -eu
. "$(dirname -- "$0")"/util.sh
selfdir=$(cd -P -- "$(dirname -- "$0")"; pwd -P)
. "$selfdir"/util.sh


usage()
{
	name=$(basename "$(readlinkf "$0")")
	cat <<EOF | text_format
**NAME**
    $(basename -- "$0") - Extract line from stdin between two patterns
    $name - Extract line from stdin between two patterns

**SYNOPSIS**
    $(basename -- "$0") [**-beh**] __PAT_BEGIN__ __PAT_END__
    $name [**-beh**] __PAT_BEGIN__ __PAT_END__

**DESCRIPTION**
    Read stdin and extract all the lines between __PAT_BEGIN__ and __PAT_END__


@@ 39,24 41,16 @@ incl_end=false
while getopts "beh" OPT
do
	case "$OPT" in
		b)
			incl_beg=true
			;;
		e)
			incl_end=true
			;;
		h)
			usage 0
			;;
		\?)
			usage 1
			;;
		b)  incl_beg=true;;
		e)  incl_end=true;;
		h)  usage 0;;
		\?) usage 1;;
	esac
done
shift $((OPTIND - 1))
[ $# -ne 2 ] && usage 1

awk "
    /$1/ $(ter $incl_beg '{flag = 1}' '{flag = 1; next}')
	/$2/ $(ter $incl_end '{flag = 0; print}' '{flag = 0}')
	/$1/ $($incl_beg && pecho '{flag = 1}'        || pecho '{flag = 1; next}')
	/$2/ $($incl_end && pecho '{flag = 0; print}' || pecho '{flag = 0}')
	flag"

M bin/rename.sh => bin/rename.sh +10 -12
@@ 2,17 2,19 @@
# Dependencies: util.sh
# Portability:  POSIX
set -eu
. "$(dirname -- "$0")"/util.sh
selfdir=$(cd -P -- "$(dirname -- "$0")"; pwd -P)
. "$selfdir"/util.sh


usage()
{
	name=$(basename "$(readlinkf "$0")")
	cat <<EOF | text_format
**NAME**
    $(basename -- "$0") - Rename files easily
    $name - Rename files easily

**SYNOPSIS**
    $(basename -- "$0") [**-sh**] __OUT_FMT__ [__FILE__...]
    $name [**-sh**] __OUT_FMT__ [__FILE__...]

**DESCRIPTION**
    Rename __FILE__s to __OUT_FMT__, which will be passed to eval with the


@@ 56,12 58,8 @@ do
			i=$OPTARG
			! match "$i" '[0-9]+' && die "$i: not an integer"
			;;
		h)
			usage 0
			;;
		\?)
			usage 1
			;;
		h)  usage 0;;
		\?) usage 1;;
	esac
done
shift $((OPTIND - 1))


@@ 71,16 69,16 @@ fmt=$1
shift

files=$(readargs "$@")
width=$(echop "$files" | wc -l)
width=$(pecho "$files" | wc -l)
width=${#width}

echop "$files" | while IFS= read -r path
pecho "$files" | while IFS= read -r path
do
	base=$(file_base "$path")
	ext=$(file_ext "$path")
	dir=$(dirname -- "$path")
	count=$(printf "%0${width}d" "$i")
	target=$(eval echop "$fmt")
	target=$(eval pecho "$fmt")
	if [ "$path" != "$target" ]
	then
		mv -f -- "$path" "$target"

M bin/util.sh => bin/util.sh +340 -72
@@ 1,39 1,39 @@
# Various utilities for other POSIX sh scripts
#
# Portability:
#     xargsnl: GNU, *BSD, MacOS, Illumos

# +----------+-----+---------+--------+--------------+---------+-------+---------+-------+-----+------+-------+
# | cmd/OS   | GNU | OpenBSD | NetBSD | DragonflyBSD | FreeBSD | MacOS | Illumos | HP-UX | AIX | IRIX | Tru64 |
# +----------+-----+---------+--------+--------------+---------+-------+---------+-------+-----+------+-------+
# | xargs -0 | o   | o       | o      | o            | o       | o     | o       |       |     |      |       |
# +----------+-----+---------+--------+--------------+---------+-------+---------+-------+-----+------+-------+
set -u


alias parallel='parallel --no-notice'
alias ffmpeg='ffmpeg -hide_banner'
alias ffprobe='ffprobe -hide_banner'

# Portable echo, without any option
echop()
pecho()
{
	printf '%s\n' "$*"
}

# Ternary operator, output $2 if $1 evaluates to true, $3 otherwise (if $3 there
# is)
ter()
{
	if eval "$1" >/dev/null 2>&1
	then
		echop "$2"
	else
		if [ $# -eq 3 ]
		then
			echop "$3"
		fi
	fi
}


# Print all arguments prefixed by the program name to stderr and exits with
# status 1
die()
{
	echop "[$(basename -- "$0")]" "$@" >&2
	pecho "[$(basename "$(readlinkf "$0")")] $*" >&2
	exit 1
}

# Returns 0 if $1 matches $2 (as an ERE), 1 otherwise
match()
{
	pecho "$1" | grep -Eqx -- "$2"
}

# Die with an appropriate message if "test $1" returns false for any of the
# remaining argument paths or if one of these doesn't exists
requirefile()


@@ 42,54 42,74 @@ requirefile()
	shift
	for _i
	do
		[ ! -e "$_i" ] && die "$_i: file not found"
		if [ ! "$_testarg" "$_i" ]
		[ ! -L "$_i" ] && [ ! -e "$_i" ] && die "$_i: file not found"
		if [ "$_testarg" != "-e" ] && [ ! "$_testarg" "$_i" ]
		then
			case "$_testarg" in
				-b)
					die "$_i: not a block device"
					;;
				-c)
					die "$_i: not a character special file"
					;;
				-d)
					die "$_i: not a directory"
					;;
				-e|-f)
					die "$_i: not a regular file"
					;;
				-g)
					die "$_i: not a setgid file"
					;;
				-h|-L)
					die "$_i: not a symbolic link"
					;;
				-p)
					die "$_i: not a FIFO"
					;;
				-r)
					die "$_i: not a readable file"
					;;
				-S)
					die "$_i: not a socket"
					;;
				-s)
					die "$_i: not a non-empty file"
					;;
				-u)
					die "$_i: not a setuid file"
					;;
				-w)
					die "$_i: not a writeable file"
					;;
				-x)
					die "$_i: not a executable file"
					;;
				-b)    die "$_i: not a block device";;
				-c)    die "$_i: not a character special file";;
				-d)    die "$_i: not a directory";;
				-f)    die "$_i: not a regular file";;
				-g)    die "$_i: not a setgid file";;
				-h|-L) die "$_i: not a symbolic link";;
				-p)    die "$_i: not a FIFO";;
				-r)    die "$_i: not a readable file";;
				-S)    die "$_i: not a socket";;
				-s)    die "$_i: not a non-empty file";;
				-u)    die "$_i: not a setuid file";;
				-w)    die "$_i: not a writeable file";;
				-x)    die "$_i: not a executable file";;
			esac
		fi
	done
}

checkbin()
{
	command -v "$1" >/dev/null
}

# For each argument, check for executable presence; use a,b,c... for alternatives
requirebin()
{
	for _i
	do
		if match "$_i" '.*|.*'
		then
			_i=$(pecho "$_i" | tr ',' ' ')
			_success=false
			for _j in $_i
			do
				if checkbin "$_j"
				then
					_success=true
				fi
			done
			if ! $_success
			then
				echo "At least one executable amongst `$_i` required"
				return 1
			fi
		elif ! checkbin "$_i"
		then
			echo "$_i: executable not found"
			return 1
		fi
	done
}

# Die with an appropriate message if one of the argument paths exists
forbidfile()
{
	for _i
	do
		if [ -L "$_i" ] || [ -e "$_i" ]
		then
			die "$_i: file already exists"
		fi
	done
}

# Output all arguments separated by '\n'. If there is no argument or the only
# argument is '-', output stdin instead
readargs()


@@ 102,27 122,82 @@ readargs()
	fi
}

# Quote all arguments
quote()
{
	pecho "$1" | sed "s#'#'\\\\''#g; s#^#'#; s#\$#'#"
}

# Escape all BRE metacharacters in $1
bre_escape()
{
	pecho "$1" | sed 's#[[^$*.\\-]#\\&#g'
}

# Escape all BRE metacharacters in $1
ere_escape()
{
	pecho "$1" | sed 's#[()|{}+?[^$*.\\-]#\\&#g'
}

# Escape all sed substitute metacharacters in $1
sed_repl_escape()
{
	pecho "$1" | sed 's#[\\&]#\\&#g'
}

# Escape all globbing metacharacters in $1
glob_escape()
{
    pecho "$1" | sed 's#[[*?]#\\&#g'
}

# List the absolute path of all files in $1 separated by \n. The remaining
# arguments are passed to find.
listfiles()
{
	_dir=$1
	shift
	_esc=$(glob_escape "$_dir")
	find -- "$_dir" \( ! -path "$_esc" -prune \) "$@"
}

# xargs -d'\n' for *BSD/GNU/Solaris
xargsnl()
{
	tr '\n' '\000' | xargs -0 "$@"
}

# Convert all arguments to lowercase
tolower()
{
	readargs "$@" | tr '[:upper:]' '[:lower:]'
}

# Returns 0 if $1 matches $2 (as an ERE), 1 otherwise
match()
# Portable head -n-val
headneg()
{
	echop "$1" | grep -Eqx -- "$2"
	! match "$1" '[[:digit:]]+' && return 1
	awk -varg=$1 '{if(NR > arg) print buf[NR % arg]; buf[NR % arg] = $0}'
}

# Format stdin so that __foo_bar__ is underlined and **foo*bar** is emboldened
text_format()
{
	set +e
	_sgr0=$(tput sgr0)
	_bold=$(tput bold)
	_smul=$(tput smul)
	_rmul=$(tput rmul)
	set -e
	if [ -t 1 ]
	then
		set +e
		_sgr0=$(tput sgr0)
		_bold=$(tput bold)
		_smul=$(tput smul)
		_rmul=$(tput rmul)
		set -e
	else
		_sgr0=
		_bold=
		_smul=
		_rmul=
	fi
	sed -E \
		-e ":a; s#([^_]|^)__(([^_]*(_[^_])*[^_]*)+)__([^_]|\$)#\1$_smul\2$_rmul\5#; ta" \
		-e ":b; s#([^*]|^)\*\*(([^\*]*(\*[^*])*[^*]*)+)\*\*([^*]|\$)#\1$_bold\2$_sgr0\5#; tb"


@@ 132,7 207,13 @@ text_format()
# e.g. "foo.tar.gz" returns "gz")
file_ext()
{
	echop "$1" | sed -n 's#.*[^/]\.\([^.]*\)$#\1#p'
	pecho "$1" | sed -n 's#.*[^/]\.\([^.]*\)$#\1#p'
}

# Print the mimetype of $1
mimetype()
{
	file --dereference --brief --mime-type -- "$1"
}

# Extract the basename of a filename (minus the extension)


@@ 142,14 223,201 @@ file_base()
	_base=${_tmp%.*}
	if [ ! "$_base" ]
	then
		echop "$_tmp"
		pecho "$_tmp"
	else
		echop "$_base"
		pecho "$_base"
	fi
}

# Read a password securely into variable $2 using $1 as a prompt
read_password()
{
	printf '%s ' "$1"
	stty -echo
	read -r "$2"
	stty echo
	echo
}

# Takes argv ("$@") as argument and determines if the help option is called
is_help()
{
	[ $# -eq 1 ] && ([ "$1" = "-h" ] || [ "$1" = "--help" ])
}

# Print stdin lines as a pretty list: "line1, line2, ..."
list_join()
{
	_sep=${1:-, }
	sed ':a; N; $!ba; s#\n#'"$(sed_repl_escape "$_sep")"'#g'
}

# Execute $@ with sh then outputs the number of seconds it took
ptime()
{
	command time -p sh -c '{ "$@";} 2>&3 >/dev/null' dummy "$@" 3>&2 2>&1 | \
		sed -n 's#^real ##p'
}

# prints a random unsigned number of $1 bytes
rand()
{
	requirefile -c /dev/urandom
	od -An -N$1 -t u$1 /dev/urandom | tr -d '[:blank:]'
}

# IFS split $1 into the variables $2, $3, ...
read_split()
{
	_str=$1
	shift
	read -r "$@" <<-EOF
		$_str
EOF
}

# Something a bit like select. The selection is read on stdin and the result is
# printed to stdout
pselect()
{
	_in=$(cat)
	_len=$(pecho "$_in" | wc -l)
	while true
	do
		pecho "$_in" | awk '{printf "%d) %s\n", NR, $0}' >&2
		printf '\n#?' >&2
		read -r _choice </dev/tty
		if ! match "$_choice" '[0-9]+' || [ $_choice -gt $_len ]
		then
			printf '%s: invalid value\n' "$_choice" >&2
			continue
		fi
	    pecho "$_in" | sed -n "$_choice"'{p; q}'
		break
	done
}

# GNU parallel handling nested instances
parallel()
{
	if [ "${PARALLEL_PID:-}" ]
	then
		command env PARALLEL=$(pecho "$PARALLEL" | sed 's#--eta##g') \
			parallel -j1 "$@"
	else
		command parallel "$@"
	fi
}

# Like the usual tac(1), print lines in reverse
tac()
{
	awk '{line[NR] = $0} END {for (i = NR; i; --i) {print line[i]}}' "$@"
}

# Recursively delete empty directories
rmdir_recursive()
{
	for i
	do
		find -- "$i" -type d | awk -F/ '{print NF, $0}' | sort -k1 -n | rev | \
			cut -d' ' -f2- | xargsnl rmdir --
	done
}

# Simple portable seq without fancy options (use list_join to get -s)
seq()
{
	_incr=1
	_first=1
	case $# in
		1)  _last=$1;;
		2)
			_first=$1
			_last=$2
			;;
		3)
			_first=$1
			_incr=$2
			_last=$3
			;;
	esac
	while [ $_first -le $_last ]
	do
		pecho "$_first"
		_first=$((_first + _incr))
	done
}

# URL decode input or arguments
urldecode()
{
	readargs "$@" | sed 's#+# #g; s#%#\\x#g' | xargsnl printf '%b\n'
}

# Portable to Linux, *BSD and Mac
numcpu()
{
	getconf _NPROCESSORS_ONLN
}

if command readlink --version 2>/dev/null | grep -qF '^readlink (GNU coreutils)'
then
	alias readlinkf='readlink -f --'
elif checkbin greadlink
then
	alias readlinkf='greadlink -f --'
else
	# Portable readlink -f using GNU's semantics (last component need not exist)
	# Currently passes the tests from https://github.com/ko1nksm/readlinkf
	# except loop detection (`getconf SYMLOOP_MAX` is undefined here, anyway)
	readlinkf()
	{
		[ $# -eq 0 ] && return 1
		_status=0
		_pwd=$(pwd -P)
		for _i
		do
			! [ "$_i" ] && { _status=1; continue; }
			if [ -d "$_i" ]
			then
				CDPATH= cd -P -- "$_i" >/dev/null 2>&1 || return 1
				pwd -P
			else
				case "$_i" in */) [ -e "${_i%/}" ] && return 1;; esac
				while true
				do
					CDPATH= cd -P -- "$(dirname -- "$_i")" >/dev/null 2>&1 || return 1
					base=${_i%/}
					base=${base##*/}
					if [ -L "$base" ]
					then
						_i=$(ls -l -- "$base")
						_i=${_i#*"$base -> "}
					else
						dir=$(pwd -P)
						dir=${dir%/}
						printf '%s/%s\n' "$dir" "$base"
						break
					fi
				done
			fi
			cd -- "$_pwd"
		done
		return $_status
	}
fi

# Portable mktemp implementing only the -p option
pmktemp()
{
	case $# in
		0)  pecho "mkstemp(${TMPDIR:-/tmp}/tmp.XXXXXX)" | m4;;
		1)  pecho "mkstemp($1)" | m4;;
		3)
		    [ "$1" != -p ] && echo "Usage: pmktemp [-p TMPDIR] [TEMPLATE]" >&2 && return 1
		    pecho "mkstemp($2/$3)" | m4
		    ;;
		*)  return 1;;
	esac
}

M lists/B black vise.txt => lists/B black vise.txt +4 -5
@@ 5,10 5,10 @@
2	Bloodchief Ascension

#Sorceries
#CMC4
2	Damnation					(Planar Chaos)
#CMC3
2	Toxic Deluge				(Commander 2013)
#CMC2
3	Sinkhole					(1st Limited Edition Beta)
4	Sinkhole					(1st Limited Edition Beta)
4	Sign in Blood				(Magic 2013)

#Instants


@@ 33,8 33,7 @@
#Lands
20	Swamp [1]					(Ice Age)


# Damnation -> Toxic Deluge ?
# -2 Geth's Verdict, +2 Chainer's Edict?
# Underworld Dreams ?

# High budget

M lists/BG enemy lifegain.txt => lists/BG enemy lifegain.txt +2 -0
@@ 24,11 24,13 @@
4	Invigorate				(Mercadian Masques)

#Lands
4	Llanowar Wastes			(Apocalypse)
4	Overgrown Tomb			(Ravnica City of Guilds)
8	Forest [2]				(Mercadian Masques)
8	Swamp [2]				(Mercadian Masques)


# Manamorphose ?
# Regrowth ?
# Night's Whisper -> ?
# -2 Forest, -2 Swamp, +4 Llanowar Wastes

M lists/BG rock.txt => lists/BG rock.txt +4 -2
@@ 38,12 38,14 @@

#Lands
1	Oran-Rief, the Vastwood	(Zendikar)
4	Llanowar Wastes			(Apocalypse)
4	Woodland Cemetery		(Innistrad)
9	Forest [3]				(Odyssey)
6	Swamp [2]				(Odyssey)


# Llanowar Wastes -> Woodland Cemetery ?
# Alternative lands: Llanowar Wastes (Apocalypse),
#                    Overgrown Tomb (Ravnica City of Guilds),
#                    Verdant Catacombs (Zendikar)
# Erratic Portal/Cloudstone Curio ?
# Stampeding Wildebeests ?
# +2 Vines of Vastwood ?

M lists/R goblin.txt => lists/R goblin.txt +2 -2
@@ 6,7 6,7 @@
4	Goblin Ringleader		(Apocalypse)
#CMC3
4	Goblin Warchief			(Scourge)
4	Goblin King				(6th Classic Edition)
4	Goblin King				(10th Edition)
2	Goblin Sharpshooter		(Onslaught)
2	Goblin Matron			(Urza's Saga)
2	Gempalm Incinerator		(Legions)


@@ 16,7 16,7 @@
#CMC1
2	Legion Loyalist			(Gatecrash)
2	Skirk Prospector		(Onslaught)
1	Goblin Chirurgeon [3]	(Fallen Empires)
2	Goblin Chirurgeon [3]	(Fallen Empires)

#Artifacts
#CMC1

M lists/RG gate.txt => lists/RG gate.txt +6 -4
@@ 25,23 25,25 @@
#Sorceries
#CMC5
1	Overwhelming Stampede	(Magic 2011)
#CMC2
2	Hull Breach				(Planeshift)

#Instants
#CMC2
2	Destructive Revelry		(Theros)
#CMC1
3	Lightning Bolt			(1st Limited Edition Beta)
3	Vines of Vastwood		(Zendikar)

#Lands
4	Karplusan Forest		(Ice Age)
4	Rootbound Crag			(Magic 2010)
11	Forest [1] 				(Odyssey)
6	Mountain [1] 			(Odyssey)


# Alternative lands: Wooded Foothills (Onslaught), Karplusan Forest	(Ice Age)
# Saproling Burst ?
# Hull Breach -> Destructive Revelry ?
# Destructive Revelry -> Krosan Grip ?
# -2 Shivan Wurm, +2 Avalanche Riders ?
# Cloudstone Curio ?
# Keldon Marauder ?
# Undergrowth ?
# +2 Goblin Anarchomancer ?

M lists/UB mill.txt => lists/UB mill.txt +2 -0
@@ 35,6 35,8 @@
8	Swamp [2]					(Onslaught)


# -2 Smother, -2 Daze, +4 Drown in the Loch?

# Budget
# -2 Visions of Beyond, +2 Foresee
# -4 Glimpse the Unthinkable, +4 Breaking//Entering

M lists/UR ping.txt => lists/UR ping.txt +24 -24
@@ 1,45 1,45 @@
#Creatures
#CMC3
2	Chandra's Spitfire
4	Gelectrode				(Guildpact)
#CMC2
4	Thermo-Alchemist		(Eldritch Moon)
2	Young Pyromancer		(Magic 2014)
#CMC1
3	Delver of Secrets		(Innistrad)
2	Delver of Secrets		(Innistrad)
0	Insectile Aberration	(Innistrad)

#Enchantments
#CMC2
2	Alexi's Cloak
#CMC1
3	Curiosity				(Exodus)
2	Sigil of Sleep			(Urza's Destiny)

#Artifacts
#CMC1
3	Curiosity			(Exodus)
2	Sigil of Sleep		(Urza's Destiny)
1	Basilisk Collar			(Worldwake)

#Sorceries
#CMC1
3	Ponder				(Lorwyn)
3	Distortion Strike	(Rise of the Eldrazi)
2	Distortion Strike		(Rise of the Eldrazi)

#Instants
#CMC3
2	Capsize				(Tempest)
#CMC2
4	Counterspell		(Mercadian Masques)
4	Counterspell			(Mercadian Masques)
2	Boomerang				(Mirage)
2	Swerve
4	Fire//Ice			(Apocalypse)
4	Fire//Ice				(Apocalypse)
1	Fling					(Stronghold)
#CMC1
4	Lightning Bolt		(1st Limited Edition Alpha)
2	Brainstorm			(Mercadian Masques)
4	Lightning Bolt			(1st Limited Edition Alpha)
4	Brainstorm				(Mercadian Masques)

#Lands
4	Shivan Reef			(Apocalypse)
11	Island [3]			(Odyssey)
6	Mountain [4]		(Odyssey)
4	Sulfur Falls			(Innistrad)
11	Island [3]				(Odyssey)
6	Mountain [4]			(Odyssey)


# UR prowess
# Enigma Drake
# Monastery Swiftspear
# Stormchaster Mage
# Reckless Charge
# Assault Strobe / Double Cleave
# Clout of the Dominus
# Manamorphose
# Gitaxian Probe
# -4 Gelectrode, +4 Thermo-Alchemist ?
# 2 Snap ?
# Alternative lands: Shivan Reef (Apocalypse), Steam Vents (Guildpact), Scalding Tarn (Zendikar)

M lists/W parfait.txt => lists/W parfait.txt +1 -0
@@ 48,6 48,7 @@

# Hanna's Custody ?
# Armagueddon ?
# Mana Tithe ?

# Budget
# -5 Plains, +1 Serra's Sanctum, +4 Plateau

R lists/W soul sisters control.txt => lists/W soul sisters.txt +16 -18
@@ 2,11 2,11 @@
#CMC6
2	Felidar Sovereign		(Zendikar)
#CMC4
1	Dust Elemental
2	Ranger of Eos			(Shards of Alara)
#CMC3
2	Mentor of the Meek		(Innistrad)
1	Master Apothecary
1	Rune-Tail, Kitsune Ascendant_Rune-Tail's Essence
#CMC2
1	Eight-and-a-Half-Tails  (Champions of Kamigawa)
2	Grand Abolisher			(Magic 2012)


@@ 15,36 15,34 @@
4	Soul's Attendant
4	Soul Warden				(Exodus)
4	Mother of Runes			(Urza's Legacy)
2	Martyr of Sands
1	Children of Korlis

#Enchantments
#CMC4
2	Parallax Wave
#CMC3
2	Aura of Silence			(Weatherlight)
2	Martyr of Sands			(Coldsnap)
2	Children of Korlis

#Sorceries
#CMC3
2	Council's Judgment
2	Proclamation of Rebirth

#Instants
#CMC2
2	Sundering Growth		(Return to Ravnica)
#CMC1
4	Swords to Plowshares	(Ice Age)

#Lands
1	Emeria, the Sky Ruin	(Zendikar)
2	Kjeldoran Outpost		(Alliances)
17	Plains [4]				(Odyssey)
18	Plains [4]				(Odyssey)


# Serra Ascendant ?
# Dust Elemental ?
# Rally for the Throne
# Rune-Tail, Kitsune Ascendant_Rune-Tail's Essence ?
# Genesis Chamber ?
# Dawn of Hope ?
# Cleric Class ?
# Parallax Wave combo ?
# Battle Screech / Spectral Procession ?
# Hanweir Militia Captain / Westvale Cult Leader ?
# True Believer ?

# Side
# True Believer
# Competitive => +4 Serra Ascendant

# Budget
# -2 Council's Judgment, +2 Oblivion Ring
# Go B/W for Rotlung Reanimator and other goodies (Karlov, Edgewalker) ?

M lists/WG aura.txt => lists/WG aura.txt +9 -7
@@ 1,19 1,18 @@
#Creatures
#CMC3
2	Aura Gnarlid			(Rise of the Eldrazi)
#CMC4
1	Kitsune Mystic_Autumn-Tail, Kitsune Sage
#CMC2
4	Silhana Ledgewalker		(Guildpact)
4	Kor Spiritdancer		(Rise of the Eldrazi)
2	Argothian Enchantress	(Urza's Saga)
2	Kor Spiritdancer		(Rise of the Eldrazi)
#CMC1
2	Arbor Elf
2	Arbor Elf				(Worldwake)

#Enchantments
#CMC3
2	Enchantress's Presence
3	Oblivion Ring			(Lorwyn)
#CMC2
2	Sterling Grove
3	Sterling Grove

#Auras
#CMC2


@@ 25,7 24,7 @@
2	Spider Umbra			(Rise of the Eldrazi)
2	Hyena Umbra				(Rise of the Eldrazi)
2	Flickering Ward
2	Utopia Sprawl
2	Utopia Sprawl			(Dissension)

#Sorceries
#CMC5


@@ 40,6 39,9 @@
8	Plains [3]				(Tempest)
8	Forest [3]				(Tempest)

# -1 Kitsune Mystic ?
# -1 Winds of Rath ?

# Budget
# -2 Argothian Enchantress, +2 Enchantress's Presence
# -2 Daybreak Coronet, +2 Armadillo Cloak

M lists/WUR affinity.txt => lists/WUR affinity.txt +1 -1
@@ 40,7 40,7 @@
4	Ancient Den			(Mirrodin)
2	Vault of Whispers	(Mirrodin)

# +1 Hanna's Legacy ?
# +1 Lodestone Golem ?

# Low budget
# -2 Glimmervoid, +2 Spire of Industry

D lists/basic lands.txt => lists/basic lands.txt +0 -49
@@ 1,49 0,0 @@
0	Plains [1]				(Ice Age)
0	Plains [3]				(Ice Age)
0	Plains [1]				(Tempest)
0	Plains [2]				(Tempest)
0	Plains [4]				(Tempest)
0	Plains [1]				(Portal)
0	Plains [2]				(Portal)
0	Plains [3]				(Invasion)
0	Plains [4]				(Odyssey)
#0	Island [1]				(Ice Age)
0	Island [2]				(Ice Age)
0	Island [4]				(Mirage)
0	Island [1]				(Urza's Saga)
0	Island [3]				(Urza's Saga)
0	Island [3]				(Portal)
0	Island [2]				(Mercadian Masques)
0	Island [1]				(Invasion)
0	Island [3]				(Invasion)
0	Island [3]				(Odyssey)
0	Swamp [1]				(Ice Age)
0	Swamp [2]				(Ice Age)
#0	Swamp [2]				(Mirage)
0	Swamp [2]				(Tempest)
0	Swamp [2]				(Portal)
0	Swamp [3]				(Portal)
#0	Swamp [2]				(Urza's Saga)
0	Swamp [2]				(Portal Second Age)
0	Swamp [2]				(Mercadian Masques)
0	Swamp [2]				(Invasion)
0	Swamp [2]				(Odyssey)
#0	Swamp [2]				(Onslaught)
0	Mountain [3]			(1st Limited Edition Beta)
0	Mountain [1]			(Ice Age)
0	Mountain [4]			(Portal)
0	Mountain [1]			(Portal Second Age)
0	Mountain [2]			(Portal Second Age)
0	Mountain [4]			(Mercadian Masques)
0	Mountain [3]			(Invasion)
0	Mountain [4]			(Odyssey)
0	Mountain [1]			(Odyssey)
0	Forest [2]				(Ice Age)
0	Forest [3]				(Tempest)
0	Forest [1]				(Urza's Saga)
0	Forest [2]				(Urza's Saga)
0	Forest [3]				(Urza's Saga)
0	Forest [1]				(Portal Second Age)
0	Forest [3]				(Mercadian Masques)
0	Forest [4]				(Odyssey)
0	Forest [3]				(Odyssey)