Skip to content

Instantly share code, notes, and snippets.

@kou1okada
Last active January 11, 2022 11:52
Show Gist options
  • Save kou1okada/b6885013fe070b1e54a8f585779d0fe7 to your computer and use it in GitHub Desktop.
Save kou1okada/b6885013fe070b1e54a8f585779d0fe7 to your computer and use it in GitHub Desktop.
sort.sh - sort wrapper for saving original order of lines which have the same key values.
#!/usr/bin/env bash
# sort wrapper for saving original order of lines which have the same key values.
# Copyright (c) 2018 Koichi OKADA. All rights reserved.
# This script is distributed under the MIT license.
(( 5 <= DEBUG )) && set -x
function function_get_lines () # PAT_FUNC [FILE]
# Return lines of function about declare, begin and end.
{
grep -nE "^$1|^{|^}" "${@:2:1}" \
| grep -A2 -E "^[0-9]+:$1" \
| grep -o -E "^[0-9]+"
}
function headtail () # FIRST_LINE LAST_LINE
# Split lines from FIRST_LINE to LAST_LINE.
{
head -n+$2 | tail -n+$1
}
function uniqex ()
# An alternative uniq command which is not required sort
{
awk '!c[$0]++'
}
function show_options ()
{
local i
echo "Options:"
echo " OPT0:" ; for i in "${OPT0[@]}" ; do echo " $i"; done
echo " OPT1:" ; for i in "${OPT1[@]}" ; do echo " $i"; done
echo " OPT01:"; for i in "${OPT01[@]}"; do echo " $i"; done
}
function usage_default () #= [CMD [DISPLAY_NAME]]
# Auto generate default usage from documentation
# Args:
# CMD : Whole name of subcommand
{
local CMD="${1:-$CMD}"
local DISPLAY_NAME="${2:-$CMD}"
local PAT_FUNC_CMD="function +${CMD} *\( *\)"
local lines src srcs
readarray -t srcs < <(grep -lE "$PAT_FUNC_CMD" "${BASH_SOURCE[@]}" | tac | uniqex)
[ -z "$srcs" ] && { echo "Error: function ${CMD} is not founded."; exit 1; }
src="$srcs"
readarray -t lines < <(function_get_lines "$PAT_FUNC_CMD" "$src")
cat "$src" \
| headtail $(( lines[0] )) $(( lines[1] - 1 )) \
| sed -r -e 's/^(function +)('"$CMD"')( *\( *\))/\1'"$DISPLAY_NAME"'\3/g' \
| sed -r -e 's/^function +([^ ]+)[^#]*(#=? *(.*))?/Usage: \1 \3/g' \
-e 's/^#\?? (.*)/\1/g'
show_options
}
function keydefparse () # KEYDEF
# Parse KEYDEF parameter for -k and --key options of sort command, and update conditions for wrapping the sort command.
# Returns:
# TEMPLATES : templates for printf of awk.
# FIELDS : fields for printf of awk
# KEYCOUNT : number of sort keys
# KEYDEF : KEYDEF for wrapping the sort command
{
local f1 c1 opts1 f2 c2 opts2 tmp i keycount1 keycount2
shopt -q extglob; local extglob=$?; shopt -s extglob
f1="${1%%,*}"
[ "$1" != "${1/,/}" ] && f2="${1##*,}"
opts1="${f1/#*([0-9.])/}"
opts2="${f2/#*([0-9.])/}"
f1="${f1:0:${#f1}-${#opts1}}"
f2="${f2:0:${#f2}-${#opts2}}"
[ "$f1" != "${f1/./}" ] && c1="${f1#*.}"
[ "$f2" != "${f2/./}" ] && c2="${f2#*.}"
f1="${f1%.*}"
f2="${f2%.*}"
if [[ -z "$f1" || -z "$f2" ]]; then
echo "Error: KEYDEF must be given both start and stop position."
exit 1
fi
let keycount1=KEYCOUNT
for (( i = f1; i <= f2; i++ )); do
TEMPLATES+="%s,"
FIELDS+="\$${i},"
let KEYCOUNT++
done
let keycount2=KEYCOUNT-1
KEYDEF="${keycount1}${c1:+.}${c1}${opts1},${keycount2}${c2:+.}${c2}${opts2}"
[ "$extglob" = "1" ] && shopt -u extglob
}
function optparse () # [OPTIONS ...] [ARGUMENTS ...]
# Parse options of COMMAND, and update conditions for wrapping it.
# Before call this function, COMMAND must be gotten options with `get_options`.
# Returns:
# ARGS[@] : positional parameters
# OPTS[@] : options of COMMAND
{
local short param KEYDEF
shopt -q extglob; local extglob=$?; shopt -s extglob
ARGS=()
while (( 0 < $# )); do
short=
param=
if [[ "${1:0:2}" = "--" && "$1" != "${1/=/}" ]];then
param=1
set -- "${1%%=*}" "${1#*=}" "${@:2}"
elif [[ "${1:0:2}" != "--" && "${1:0:1}" = "-" && 2 < "${#1}" ]]; then
short=1
param=1
set -- "${1:0:2}" "${1:2}" "${@:2}"
fi
case "$1" in
-h|--help)
OPT_HELP="$1"
;;&
--debug)
OPT_DEBUG="$1"
;;&
-k|--key) # KEYDEF
keydefparse "$2"
OPTS+=( "$1" "$KEYDEF" )
shift 2
;;
$OPT01) # [PARAM]
if [[ "$param" = 1 ]]; then
OPTS+=( "$1=$2" )
shift 2
else
OPTS+=( "$1" )
shift 1
fi
;;
$OPT1) # PARAM
OPTS+=( "$1" "$2" )
shift 2
;;
$OPT0) #
OPTS+=( "$1" )
[[ -z "$short" && -n "$param" ]] && {
echo "Warning: taking a unexpected parameter for $1: $2"
set -- "${@:1:1}" "${@:3}"
}
[[ -n "$short" && -n "$param" ]] && set -- "${@:1:1}" "-${@:2:1}" "${@:3}"
shift 1
;;
--)
ARGS=( "${@:2}" )
shift $#
;;
-*)
echo "Error: unknown option: $1"
exit 1
;;
*)
ARGS+=( "$1" )
shift 1
;;
esac
done
[ "$extglob" = "1" ] && shopt -u extglob
}
function get_options () # <COMMAND>
# Get options of COMMAND.
# Returns:
# OPT0 : Options for case statement which take no parameters.
# OPT1 : Options for case statement which take a parameter.
# OPT01 : Options for case statement which take a optional parameter.
{
local tmp opt opts
OPT0=""
OPT1=""
OPT01=""
readarray -t opts < <("$1" --help|grep -E "^ *-"|sed -r -e 's/^ *//g' -e 's/^((-[^-])?((, +)?--[^ ,]+)*).*/\1/g')
for opt in "${opts[@]}"; do
tmp="${opt//\[=*/}"
tmp="${tmp//=*/}"
tmp="${tmp// /}"
tmp="${tmp//,/|}"
if [[ "$opt" = "${opt/=/}" ]]; then
OPT0+="${OPT0:+|}$tmp"
elif [[ "${opt}" =~ \[= ]]; then
OPT01+="${OPT01:+|}$tmp"
else
OPT1+="${OPT1:+|}$tmp"
fi
done
OPT0="@($OPT0)"
OPT1="@($OPT1)"
OPT01="@($OPT01)"
}
function main () # [OPTIONS ...] [FILES ...]
# Sort wrapper for saving original order of lines which have the same key values.
# Options:
# See help or manpage of sort command as:
# sort --help
# man sort
{
local OPT0 OPT1
local ARGS OPTS
local TEMPLATES="" FIELDS="" KEYCOUNT=1
local filter
get_options sort
optparse "$@"
if [ -n "$OPT_HELP" ]; then
usage_default "$FUNCNAME" "${0##*/}"
return
fi
if [ -n "$OPT_DEBUG" ]; then
filter=( "cat" )
else
filter=( sed -r -e 's/^([^,]*,){'"$KEYCOUNT"'}//g' )
fi
awk '{printf("'"$TEMPLATES"'%s,%s\n", '"$FIELDS"' NR, $0)}' "${ARGS[@]}" \
| sort -t, "${OPTS[@]}" -k "${KEYCOUNT}n,${KEYCOUNT}n" \
| "${filter[@]}"
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment