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 => +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 => +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)