├── README ├── add_times ├── args.c ├── bar_graph ├── cfold ├── fah-stats ├── fib.c ├── flatten ├── getmtime.c ├── lessopen ├── merge ├── mycp ├── myunlink.c ├── newscript ├── noclobber ├── nsplit ├── psort ├── remove_old ├── seg.c ├── sub ├── update_gits ├── url_decode.c └── url_encode.c /README: -------------------------------------------------------------------------------- 1 | These tools are written in C, bash 4, and POSIX awk. 2 | 3 | add_times: 4 | adds each TIME together and prints the result to stdout. 5 | 6 | args: 7 | prints command line arguments given in a very readable format 8 | 9 | bar_graph: 10 | Creates a bar graph of the categories and values in FILE(s) (or the standard 11 | input if no files are named, or if a single hyphen (-) is given), reading the 12 | first field as the category and the second as the value. Must be called with 13 | bar_grap -- [OPTIONS] [FILE...] (the -- is required). See bar_graph -- -h 14 | 15 | cfold: 16 | Wraps input lines in each FILE (standard input if not provided), writing to 17 | standard output. Also included (although slightly different) in awk-libs as 18 | an example. must be called with cfold -- [OPTIONS] [FILE...] (the -- as the 19 | first arg is required) 20 | 21 | fah-stats: 22 | parses the FAH log and gives useful information 23 | 24 | fib: 25 | prints the Nth number of the fibonacci sequence. must be compiled with -lgmp 26 | 27 | flatten: 28 | flattens a directory (recursively moves all files in sub-dirs to the current 29 | dir, and deletes the sub-dirs) 30 | 31 | getmtime: 32 | usage: getmtime FORMAT FILE [...] 33 | prints the mtime for each FILE given according to FORMAT. FORMAT is anything 34 | valid for strftime(3), as well as %N, which will be replaced with FILE. 35 | 36 | lessopen: 37 | this script is for the LESSOPEN env var, and is an "input pipe". it allows 38 | less to read archives, pdfs (with pdftotext), and will use lynx -dump for html 39 | to use it, set LESSOPEN='|lessopen %s' 40 | 41 | merge: 42 | Merge sorted FILEs into one, and print the result to the standard output. 43 | 44 | mycp: 45 | copies files. instead of erring or overwriting, appends numbers to dups 46 | (work in progress) 47 | 48 | myunlink: 49 | does a call to unlink(2), prints errno and strerr(errno) if it errs 50 | 51 | newscript: 52 | creates a new script, adding the shebang and other things for you. see 53 | newscript --help or usage() in the source code 54 | 55 | noclobber: 56 | Appends a numeric suffix to FILE, incrementing it until the resulting filename 57 | no longer exists. Prints the result to the standard output. If FILE already has 58 | an extension, adds the number before that extension. For example, if FILE is 59 | `my_file.txt', a possible result would be `my_file.1.txt'. See noclobber -h, 60 | or the comments in the source 61 | 62 | nsplit: 63 | numeric version of split(1) written in POSIX awk. must be called with 64 | nsplit -- [OPTIONS] [FILE ...] (the -- is required). see nsplit -- -h, or the 65 | comments in the source 66 | 67 | pac_removed: 68 | parses the pacman logfile and lists all removed packages that have not been 69 | reinstalled. see pac_removed -- -h, or the comments in the source 70 | 71 | psort: 72 | sort implementation that allows prioritizing the sort by pattern. 73 | see psort -- -h, or the comments in the source 74 | 75 | remove_old: 76 | removes all but the latest N files in DIR(s). 77 | 78 | seg: 79 | segfaults. for testing purposes. 80 | 81 | sub: 82 | substitutes STR for REP, treating them as literal strings. must be called 83 | with sub -- [OPTIONS] [FILE...] (the '--' as the first argument is required) 84 | 85 | update_gits: 86 | Find and update all git repositories (with git pull) within DIR, or the 87 | current working directory if DIR is not provided. By default, only check for 88 | repositories in the highest level sub-directories. Requires GNU find. 89 | 90 | url_encode: 91 | uses libcurl to escape a given URL. must be compiled with -lcurl 92 | 93 | url_decode: 94 | uses libcurl to decode a given URL. must be compiled with -lcurl 95 | 96 | TODO: 97 | extend mycp, and fix bugs 98 | 99 | 100 | 101 | 102 | 103 | Copyright Daniel Mills 104 | 105 | Permission is hereby granted, free of charge, to any person obtaining a copy 106 | of this software and associated documentation files (the "Software"), to 107 | deal in the Software without restriction, including without limitation the 108 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 109 | sell copies of the Software, and to permit persons to whom the Software is 110 | furnished to do so, subject to the following conditions: 111 | 112 | The above copyright notice and this permission notice shall be included in 113 | all copies or substantial portions of the Software. 114 | 115 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 116 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 117 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 118 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 119 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 120 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 121 | IN THE SOFTWARE. 122 | -------------------------------------------------------------------------------- /add_times: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # TODO: 4 | # add other operations (subtract, multiply, divide) 5 | 6 | # add_times [OPTIONS] [ TIME [ ... ] ] 7 | # adds each TIME together and prints the result to stdout. each TIME must be in 8 | # the format HH:MM. if no TIME is supplied, either enters a special interactive 9 | # mode if stdin is open on atty, or reads TIMEs from the standard input, one 10 | # TIME per line. 11 | # 12 | # Options: 13 | # -h, --help Display this help and exit 14 | # -s, --seconds Add seconds as well, TIME must be in the format HH:MM:SS 15 | # -S, --subtract Subtract each number, instead of adding 16 | # 17 | # in the interactive mode, add_times will act in the following manner: 18 | # if --seconds is supplied, accepts them first. then, it accepts minutes, and 19 | # finally hours. if an empty value for seconds or minutes (depending on 20 | # --seconds) is supplied, add_times stops reading input. this eases input with 21 | # the number pad 22 | 23 | shopt -s extglob 24 | 25 | # default vars 26 | oper=add # operation to perform 27 | seconds=0 # controls whether or not to use seconds 28 | times=() # times to add 29 | hrs=() # array of hour components 30 | mins=() # array of minute components 31 | secs=() # array of second components 32 | tot_secs=0 # total seconds 33 | tot_mins=0 # total minutes 34 | tot_hrs=0 # total hours 35 | 36 | # usage: is_int NUM 37 | # returns 0 if NUM is a valid integer, otherwise 1. empty values are valid 38 | is_int() { 39 | [[ $1 = *[!0-9]* ]] 40 | } 41 | 42 | # gets time interactively. returns 2 if input is to be ended, 1 if an invalid 43 | # input is given 44 | # prints the time to stdout 45 | get_time() { 46 | local h m c i 47 | 48 | if ((seconds)); then 49 | local s 50 | 51 | read -n 2 s # read in seconds 52 | # validate seconds, break on empty value 53 | if [[ -z $s ]]; then 54 | return 2 55 | elif ! is_int "$s"; then 56 | return 1 57 | fi 58 | printf '\r :%02d\r' "$s" >&2 # print seconds 59 | read -n 2 m # read in minutes 60 | is_int "$m" || return 1 # validate 61 | printf '\r :%02d:%02d\r' "$m" "$s" >&2 # print minutes and seconds 62 | # read in hours 63 | i=1 64 | while read -n 1 c && [[ $c ]]; do 65 | is_int "$c" || return 1 # validate 66 | h+=$c # append to hours 67 | printf '\r%*s:%02d:%02d\r%d' "$((++i))" ' ' "$m" "$s" "$h" >&2 # reprint 68 | done 69 | 70 | # print time to stdout 71 | printf '%02d:%02d:%02d\n' "$h" "$m" "$s" 72 | 73 | # seconds was not supplied 74 | else 75 | read -n 2 m # read in minutes 76 | # validate, break on empty value 77 | if [[ -z $m ]]; then 78 | return 2 79 | elif ! is_int "$m"; then 80 | return 1 81 | fi 82 | printf '\r :%02d\r' "$m" >&2 # print minutes 83 | # read in hours 84 | i=1 85 | while read -n 1 c && [[ $c ]]; do 86 | is_int "$c" || return 1 # validate 87 | h+=$c # append to hours 88 | printf '\r%*s:%02d\r%d' "$((++i))" ' ' "$m" "$h" >&2 # reprint 89 | done 90 | 91 | # print time to stdout 92 | printf '%02d:%02d\n' "$h" "$m" 93 | fi 94 | 95 | return 0 96 | } 97 | 98 | # show the usage info 99 | usage() { 100 | cat <<'EOF' 101 | add_times [OPTIONS] [ TIME [ ... ] ] 102 | adds each TIME together and prints the result to stdout. each TIME must be in 103 | the format HH:MM. if no TIME is supplied, either enters a special interactive 104 | mode if stdin is open on atty, or reads TIMEs from the standard input, one 105 | TIME per line. 106 | 107 | Options: 108 | -h, --help Display this help and exit 109 | -s, --seconds Add seconds as well, TIME must be in the format HH:MM:SS 110 | -S, --subtract Subtract each number, instead of adding 111 | 112 | in the interactive mode, add_times will act in the following manner: 113 | if --seconds is supplied, accepts them first. then, it accepts minutes, and 114 | finally hours. if an empty value for seconds or minutes (depending on 115 | --seconds) is supplied, add_times stops reading input. this eases input with 116 | the number pad 117 | EOF 118 | } 119 | 120 | # validate a time, based on "seconds". returns 0 if valid, otherwise 1 121 | is_valid() { 122 | local t=$1 123 | 124 | if ((seconds)); then 125 | local s m 126 | 127 | s=${t##*:} m=${t#*:} m=${m%:*} 128 | if [[ $t != +([0-9]):[0-9][0-9]:[0-9][0-9] ]] || 129 | ((s >= 60 || m >= 60)); then 130 | return 1 131 | fi 132 | else 133 | if [[ $t != +([0-9]):[0-9][0-9] ]] || ((${t#*:} >= 60)); then 134 | return 1 135 | fi 136 | fi 137 | 138 | return 0 139 | } 140 | 141 | # break options into a more parsable format 142 | optstring=hsS 143 | unset options 144 | while (($#)); do 145 | case $1 in 146 | -[!-]?*) 147 | for ((i=1; i<${#1}; i++)); do 148 | c=${1:i:1}; options+=("-$c") 149 | 150 | if [[ $optstring = *"$c:"* && ${1:i+1} ]]; then 151 | options+=("${1:i+1}") 152 | break 153 | fi 154 | done 155 | ;; 156 | --?*=*) options+=("${1%%=*}" "${1#*=}");; 157 | # end of options, stop breaking them up 158 | --) 159 | options+=(--endopts) 160 | shift 161 | options+=("$@") 162 | break 163 | ;; 164 | *) options+=("$1");; 165 | esac 166 | 167 | shift 168 | done 169 | set -- "${options[@]}" 170 | unset options 171 | 172 | # actually parse the options and do stuff 173 | while [[ $1 = -?* ]]; do 174 | case $1 in 175 | -h|--help) usage >&2; exit 0;; 176 | -s|--seconds) seconds=1;; 177 | -S|--subtract) oper=sub;; 178 | --endopts) shift; break;; 179 | *) printf 'invalid option: %s\n' "$1" >&2; exit 1;; 180 | esac 181 | 182 | shift 183 | done 184 | 185 | # TIME is supplied, use the args 186 | if (($#)); then 187 | # validate each and append to array 188 | for t; do 189 | if ! is_valid "$t"; then 190 | printf 'invalid time: %s\n' "$t" 191 | exit 1 192 | fi 193 | 194 | times+=("$t") 195 | done 196 | 197 | # no TIME is supplied... 198 | else 199 | # stdin is a tty, get them interactively 200 | if [[ -t 0 ]]; then 201 | while t=$(get_time); r=$?; ((r < 2)); do 202 | if ((r)) || ! is_valid "$t"; then 203 | printf 'invalid time: %s\n' "$t" 204 | continue 205 | fi 206 | 207 | times+=("$t") 208 | done 209 | 210 | # stdin is not a tty, just read one per line 211 | else 212 | while read -r t; do 213 | if ! is_valid "$t"; then 214 | printf 'invalid time: %s\n' "$t" 215 | exit 1 216 | fi 217 | 218 | times+=("$t") 219 | done 220 | fi 221 | fi 222 | 223 | # add or subtract based on $oper 224 | case $oper in 225 | add) 226 | # break TIMEs into hours and minutes, and possibly seconds 227 | hrs=("${times[@]%%:*}") 228 | if ((seconds)); then 229 | mins=("${times[@]#*:}") 230 | mins=("${mins[@]%:*}") 231 | secs=("${times[@]##*:}") 232 | else 233 | mins=("${times[@]#*:}") 234 | fi 235 | 236 | # if seconds is used, add totals, get end result, and add what's needed to mins 237 | if ((seconds)); then 238 | for s in "${secs[@]}"; do 239 | ((tot_secs += s)) 240 | done 241 | 242 | tot_mins+=$((tot_secs / 60)) 243 | tot_secs=$((tot_secs % 60)) 244 | fi 245 | 246 | # add total minutes 247 | for m in "${mins[@]}"; do 248 | ((tot_mins += m)) 249 | done 250 | 251 | # get end result for mins (modulo of 60), and what to add to hrs 252 | tot_hrs+=$((tot_mins / 60)) 253 | tot_mins=$((tot_mins % 60)) 254 | 255 | # add each hour component to tot_hrs 256 | for h in "${hrs[@]}"; do 257 | ((tot_hrs += h)) 258 | done 259 | 260 | # print final time 261 | if ((seconds)); then 262 | printf '%02d:%02d:%02d\n' "$tot_hrs" "$tot_mins" "$tot_secs" 263 | else 264 | printf '%02d:%02d\n' "$tot_hrs" "$tot_mins" 265 | fi 266 | ;; 267 | sub) 268 | ;; 269 | # somehow, oper is invalid (should never happen) 270 | *) 271 | printf 'invalid operation: %s\n' "$oper" >&2 272 | exit 1 273 | ;; 274 | esac 275 | 276 | 277 | 278 | # Copyright Daniel Mills 279 | # 280 | # Permission is hereby granted, free of charge, to any person obtaining a copy 281 | # of this software and associated documentation files (the "Software"), to 282 | # deal in the Software without restriction, including without limitation the 283 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 284 | # sell copies of the Software, and to permit persons to whom the Software is 285 | # furnished to do so, subject to the following conditions: 286 | # 287 | # The above copyright notice and this permission notice shall be included in 288 | # all copies or substantial portions of the Software. 289 | # 290 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 291 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 292 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 293 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 294 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 295 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 296 | # IN THE SOFTWARE. 297 | -------------------------------------------------------------------------------- /args.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main(int argc, char *argv[]) { 5 | int i; 6 | 7 | printf("%d arg%s:", argc - 1, argc == 2 ? "" : "s"); 8 | for (i=1; i", argv[i]); 10 | } 11 | printf("\n"); 12 | 13 | return 0; 14 | } 15 | 16 | 17 | 18 | /* 19 | Copyright Daniel Mills 20 | 21 | Permission is hereby granted, free of charge, to any person obtaining a copy 22 | of this software and associated documentation files (the "Software"), to 23 | deal in the Software without restriction, including without limitation the 24 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 25 | sell copies of the Software, and to permit persons to whom the Software is 26 | furnished to do so, subject to the following conditions: 27 | 28 | The above copyright notice and this permission notice shall be included in 29 | all copies or substantial portions of the Software. 30 | 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 32 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 33 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 34 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 35 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 36 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 37 | IN THE SOFTWARE. 38 | */ 39 | -------------------------------------------------------------------------------- /bar_graph: -------------------------------------------------------------------------------- 1 | #!/usr/bin/awk -f 2 | 3 | # TODO: 4 | # --total? 5 | # option to turn scale on or off? 6 | # option to round to ints? 7 | 8 | # Usage: bar_graph -- [OPTIONS] [FILE...] 9 | # 10 | # The '--' is required, so AWK itself doesn't read the options. 11 | # 12 | # Creates a bar graph of the categories and values in FILE(s) (or the standard 13 | # input if no files are named, or if a single hyphen (-) is given), reading the 14 | # first field as the category and the second as the value. If multiple lines 15 | # with the same category exist, the values are added as each is encountered. 16 | # Negative values will be treated as zero. The graph width is determined by the 17 | # terminal width. If the standard input is not a terminal, and --width is not 18 | # used, the default is 80 characters. 19 | # 20 | # Options: 21 | # -d, --delim PATT Use the AWK style regular expression PATT as the 22 | # field separator, in the same manner of AWK's FS. If 23 | # -d is not provided, the default FS is used 24 | # -t, --title TITLE Add the title TITLE to the graph 25 | # -b, --bar STRING Use STRING for the bars of the graph, instead of 26 | # "=", repeating it as needed. May use a partial value 27 | # at the end if STRING is not a single character 28 | # -n, --nums Use the values (or percentages, if -p is used) as 29 | # the final part of the bar 30 | # -p, --percent Graph values as percentages of the largest (largest 31 | # value being 100%) 32 | # -P, --percent-tot Graph values as percentages of the total 33 | # -s, --sort[=HOW] Sort the categories, according to HOW, if provided. 34 | # If not provided, the default is "std asc". Values 35 | # for HOW are explained below 36 | # -i, --ignorecase Ignore the case of the categories. The case used for 37 | # the category in the graph will be that of the first 38 | # encountered in the data 39 | # -w, --width WIDTH Force a graph width of WIDTH. WIDTH is an int >= 40 40 | # -l, --cat-label LABEL Label the category axis with LABEL 41 | # -v, --val-label LABEL Label the value axis with LABEL 42 | # -S, --stty Use "stty size" to determine the terminal width 43 | # instead of "tput cols" 44 | # -c, --color[=WHEN] Use colored output. WHEN is always, auto, or never 45 | # -B, --bold[=WHEN] Format the output using boldness. WHEN is always, 46 | # auto, or never 47 | # -C, --colors STRING Use the colors in STRING to color the graph for -c. 48 | # STRING is a list of characters, each representing a 49 | # color. The colors will be used for each line in 50 | # order, repeating until all of the categories are 51 | # exhausted. Valid values are explained below. 52 | # -h, --help Display this help and exit 53 | # 54 | # Values for --sort=HOW: 55 | # "std asc" 56 | # use AWK's standard rules for comparison, ascending. this is the default 57 | # "std desc" 58 | # use AWK's standard rules for comparison, descending. 59 | # "str asc" 60 | # force comparison as strings, ascending. 61 | # "str desc" 62 | # force comparison as strings, descending. 63 | # "num asc" 64 | # force a numeric comparison, ascending. 65 | # "num desc" 66 | # force a numeric comparison, descending. 67 | # "val asc" 68 | # sort by value, instead of category, in the same manner as "num asc". 69 | # "val desc" 70 | # sort by value, instead of category, in the same manner as "num desc". 71 | # 72 | # Values for --colors: 73 | # "B": black 74 | # "r": red 75 | # "g": green 76 | # "y": yellow 77 | # "b": blue 78 | # "m": magenta 79 | # "c": cyan 80 | # "w": white 81 | # "d": the default terminal color 82 | # For example, the default color string is "rgbyCm" 83 | 84 | # comparison function 85 | # compares "a" and "b" based on "how", returning 0 for false and 1 for true. 86 | # required for all of the qsort() functions below 87 | function __compare(a, b, how) { 88 | # standard comparisons 89 | if (how == "std asc") { 90 | return a < b; 91 | } else if (how == "std desc") { 92 | return a > b; 93 | 94 | # force string comps 95 | } else if (how == "str asc") { 96 | return "a" a < "a" b; 97 | } else if (how == "str desc") { 98 | return "a" a > "a" b; 99 | 100 | # force numeric 101 | } else if (how == "num asc") { 102 | return +a < +b; 103 | } else if (how == "num desc") { 104 | return +a > +b; 105 | } 106 | } 107 | 108 | # actual sorting function 109 | # sorts the values in "array" in-place, from indices "left" to "right", based 110 | # on the comparison mode "how" (see the qsort() description). 111 | # required for all of the qsort() functions below 112 | function __quicksort(array, left, right, how, piv, mid, tmp) { 113 | # return if array contains one element or less 114 | if ((right - left) <= 0) { 115 | return; 116 | } 117 | 118 | # choose random pivot 119 | piv = int(rand() * (right - left + 1)) + left; 120 | 121 | # swap left and pivot 122 | tmp = array[piv]; 123 | array[piv] = array[left]; 124 | array[left] = tmp; 125 | 126 | mid = left; 127 | # iterate over each element from the second to the last, and compare 128 | for (piv=left+1; piv<=right; piv++) { 129 | # if the comparison based on "how" is true... 130 | if (__compare(array[piv], array[left], how)) { 131 | # increment mid 132 | mid++; 133 | 134 | # swap mid and pivot 135 | tmp = array[piv]; 136 | array[piv] = array[mid]; 137 | array[mid] = tmp; 138 | } 139 | } 140 | 141 | # swap left and mid 142 | tmp = array[mid]; 143 | array[mid] = array[left]; 144 | array[left] = tmp; 145 | 146 | # recursively sort the two halves 147 | __quicksort(array, left, mid - 1, how); 148 | __quicksort(array, mid + 1, right, how); 149 | } 150 | 151 | # actual sorting function for "val *" sorting 152 | function __quicksort_val(array, values, left, right, how, piv, mid, tmp) { 153 | # return if array contains one element or less 154 | if ((right - left) <= 0) { 155 | return; 156 | } 157 | 158 | # choose random pivot 159 | piv = int(rand() * (right - left + 1)) + left; 160 | 161 | # swap left and pivot 162 | tmp = array[piv]; 163 | array[piv] = array[left]; 164 | array[left] = tmp; 165 | tmp = values[piv]; 166 | values[piv] = values[left]; 167 | values[left] = tmp; 168 | 169 | mid = left; 170 | # iterate over each element from the second to the last, and compare 171 | for (piv=left+1; piv<=right; piv++) { 172 | # if the comparison based on "how" is true... 173 | if (__compare(values[piv], values[left], how)) { 174 | # increment mid 175 | mid++; 176 | 177 | # swap mid and pivot 178 | tmp = array[piv]; 179 | array[piv] = array[mid]; 180 | array[mid] = tmp; 181 | tmp = values[piv]; 182 | values[piv] = values[mid]; 183 | values[mid] = tmp; 184 | } 185 | } 186 | 187 | # swap left and mid 188 | tmp = array[mid]; 189 | array[mid] = array[left]; 190 | array[left] = tmp; 191 | tmp = values[mid]; 192 | values[mid] = values[left]; 193 | values[left] = tmp; 194 | 195 | # recursively sort the two halves 196 | __quicksort_val(array, values, left, mid - 1, how); 197 | __quicksort_val(array, values, mid + 1, right, how); 198 | } 199 | 200 | ## usage: ceil(number) 201 | ## returns "number" rounded UP to the nearest int 202 | function ceil(num) { 203 | if (num < 0) { 204 | return int(num); 205 | } else { 206 | return int(num) + (num == int(num) ? 0 : 1); 207 | } 208 | } 209 | 210 | ## usage: center(string [, width]) 211 | ## returns "string" centered based on "width". if "width" is not provided (or 212 | ## is 0), uses the width of the terminal, or 80 if standard output is not open 213 | ## on a terminal. 214 | ## note: does not check the length of the string. if it's wider than the 215 | ## terminal, it will not center lines other than the first. for best results, 216 | ## combine with fold() (see the cfold script in the examples directory for a 217 | ## script that does exactly this) 218 | function center(str, cols, off, cmd) { 219 | if (!cols) { 220 | # checks if stdout is a tty 221 | if (system("test -t 1")) { 222 | cols = 80; 223 | } else { 224 | cmd = "tput cols"; 225 | cmd | getline cols; 226 | close(cmd); 227 | } 228 | } 229 | 230 | off = int((cols/2) + (length(str)/2)); 231 | 232 | return sprintf("%*s", off, str); 233 | } 234 | 235 | # usage: check_when(when) 236 | # checks "when" for "always", "auto", or "never", and uses isatty() to check 237 | # if the standard output is open on a tty. returns 1 if the option should be 238 | # set, 0 if not, and -1 if "when" is invalid 239 | function check_when(when) { 240 | if (when == "always") { 241 | return 1; 242 | } else if (when == "auto") { 243 | if (isatty(1)) { 244 | return 1; 245 | } else { 246 | return 0; 247 | } 248 | } else if (when == "never") { 249 | return 0; 250 | } else { 251 | return -1 252 | } 253 | } 254 | 255 | ## usage: fold(string, sep [, width]) 256 | ## returns "string", wrapped, with lines broken on "sep" to "width" columns. 257 | ## "sep" is a list of characters to break at, similar to IFS in a POSIX shell. 258 | ## if "sep" is empty, wraps at exactly "width" characters. if "width" is not 259 | ## provided (or is 0), uses the width of the terminal, or 80 if standard output 260 | ## is not open on a terminal. 261 | ## note: currently, tabs are squeezed to a single space. this will be fixed 262 | function fold(str, sep, cols, out, cmd, i, len, chars, c, last, f, first) { 263 | if (!cols) { 264 | # checks if stdout is a tty 265 | if (system("test -t 1")) { 266 | cols = 80; 267 | } else { 268 | cmd = "tput cols"; 269 | cmd | getline cols; 270 | close(cmd); 271 | } 272 | } 273 | 274 | # squeeze tabs and newlines to spaces 275 | gsub(/[\t\n]/, " ", str); 276 | 277 | # if "sep" is empty, just fold on cols with substr 278 | if (!length(sep)) { 279 | len = length(str); 280 | 281 | out = substr(str, 1, cols); 282 | for (i=cols+1; i<=len; i+=cols) { 283 | out = out "\n" substr(str, i, cols); 284 | } 285 | 286 | return out; 287 | 288 | # otherwise, we have to loop over every character (can't split() on sep, it 289 | # would destroy the existing separators) 290 | } else { 291 | # split string into char array 292 | len = split(str, chars, ""); 293 | # set boolean, used to assign the first line differently 294 | first = 1; 295 | 296 | for (i=1; i<=len; i+=last) { 297 | f = 0; 298 | for (c=i+cols-1; c>=i; c--) { 299 | if (index(sep, chars[c])) { 300 | last = c - i + 1; 301 | f = 1; 302 | break; 303 | } 304 | } 305 | 306 | if (!f) { 307 | last = cols; 308 | } 309 | 310 | if (first) { 311 | out = substr(str, i, last); 312 | first = 0; 313 | } else { 314 | out = out "\n" substr(str, i, last); 315 | } 316 | } 317 | } 318 | 319 | # return the output 320 | return out; 321 | } 322 | 323 | ## usage: getopts(optstring [, longopt_array ]) 324 | ## Parses options, and deletes them from ARGV. "optstring" is of the form 325 | ## "ab:c". Each letter is a possible option. If the letter is followed by a 326 | ## colon (:), then the option requires an argument. If an argument is not 327 | ## provided, or an invalid option is given, getopts will print the appropriate 328 | ## error message and return "?". Returns each option as it's read, and -1 when 329 | ## no options are left. "optind" will be set to the index of the next 330 | ## non-option argument when finished. "optarg" will be set to the option's 331 | ## argument, when provided. If not provided, "optarg" will be empty. "optname" 332 | ## will be set to the current option, as provided. Getopts will delete each 333 | ## option and argument that it successfully reads, so awk will be able to treat 334 | ## whatever's left as filenames/assignments, as usual. If provided, 335 | ## "longopt_array" is the name of an associative array that maps long options to 336 | ## the appropriate short option (do not include the hyphens on either). 337 | ## Sample usage can be found in the examples dir, with gawk extensions, or in 338 | ## the ogrep script for a POSIX example: https://github.com/e36freak/ogrep 339 | function getopts(optstring, longarr, opt, trimmed, hasarg, repeat) { 340 | hasarg = repeat = 0; 341 | optarg = ""; 342 | # increment optind 343 | optind++; 344 | 345 | # return -1 if the current arg is not an option or there are no args left 346 | if (ARGV[optind] !~ /^-/ || optind >= ARGC) { 347 | return -1; 348 | } 349 | 350 | # if option is "--" (end of options), delete arg and return -1 351 | if (ARGV[optind] == "--") { 352 | for (i=1; i<=optind; i++) { 353 | delete ARGV[i]; 354 | } 355 | return -1; 356 | } 357 | 358 | # if the option is a long argument... 359 | if (ARGV[optind] ~ /^--/) { 360 | # trim hyphens 361 | trimmed = substr(ARGV[optind], 3); 362 | # if of the format --foo=bar, split the two. assign "bar" to optarg and 363 | # set hasarg to 1 364 | if (trimmed ~ /=/) { 365 | optarg = trimmed; 366 | sub(/=.*/, "", trimmed); sub(/^[^=]*=/, "", optarg); 367 | hasarg = 1; 368 | } 369 | 370 | # invalid long opt 371 | if (!(trimmed in longarr)) { 372 | printf("unrecognized option -- '%s'\n", ARGV[optind]) > "/dev/stderr"; 373 | return "?"; 374 | } 375 | 376 | opt = longarr[trimmed]; 377 | # set optname by prepending dashes to the trimmed argument 378 | optname = "--" trimmed; 379 | 380 | # otherwise, it is a short option 381 | } else { 382 | # remove the hyphen, and get just the option letter 383 | opt = substr(ARGV[optind], 2, 1); 384 | # set trimmed to whatevers left 385 | trimmed = substr(ARGV[optind], 3); 386 | 387 | # invalid option 388 | if (!index(optstring, opt)) { 389 | printf("invalid option -- '%s'\n", opt) > "/dev/stderr"; 390 | return "?"; 391 | } 392 | 393 | # if there is more to the argument than just -o 394 | if (length(trimmed)) { 395 | # if option requires an argument, set the rest to optarg and hasarg to 1 396 | if (index(optstring, opt ":")) { 397 | optarg = trimmed; 398 | hasarg = 1; 399 | 400 | # otherwise, prepend a hyphen to the rest and set repeat to 1, so the 401 | # same arg is processed again without the first option 402 | } else { 403 | ARGV[optind] = "-" trimmed; 404 | repeat = 1; 405 | } 406 | } 407 | 408 | # set optname by prepending a hypen to opt 409 | optname = "-" opt; 410 | } 411 | 412 | # if the option requires an arg and hasarg is 0 413 | if (index(optstring, opt ":") && !hasarg) { 414 | # increment optind, check if no arguments are left 415 | if (++optind >= ARGC) { 416 | printf("option requires an argument -- '%s'\n", optname) > "/dev/stderr"; 417 | return "?"; 418 | } 419 | 420 | # set optarg 421 | optarg = ARGV[optind]; 422 | 423 | # if repeat is set, decrement optind so we process the same arg again 424 | # mutually exclusive to needing an argument, otherwise hasarg would be set 425 | } else if (repeat) { 426 | optind--; 427 | } 428 | 429 | # delete all arguments up to this point, just to make sure 430 | for (i=1; i<=optind; i++) { 431 | delete ARGV[i]; 432 | } 433 | 434 | # return the option letter 435 | return opt; 436 | } 437 | 438 | ## usage: isatty(fd) 439 | ## Checks if "fd" is open on a tty. Returns 1 if so, 0 if not, and -1 if an 440 | ## error occurs 441 | function isatty(fd) { 442 | # make sure fd is an int 443 | if (fd !~ /^[0-9]+$/) { 444 | return -1; 445 | } 446 | 447 | # actually test 448 | return !system("test -t " fd); 449 | } 450 | 451 | ## usage: isint(string) 452 | ## returns 1 if "string" is a valid integer, otherwise 0 453 | function isint(str) { 454 | if (str !~ /^-?[0-9]+$/) { 455 | return 0; 456 | } 457 | 458 | return 1; 459 | } 460 | 461 | ## usage: qsorti(s, d [, how]) 462 | ## sorts the indices in the array "s" using awk's normal rules for comparing 463 | ## values, creating a new sorted array "d" indexed with sequential integers 464 | ## starting with 1, and with the values the indices of "s". returns the length, 465 | ## or -1 if an error occurs.. leaves the source array "s" unchanged. the 466 | ## optional string "how" controls the direction and the comparison mode. uses 467 | ## the quick sort algorithm, with a random pivot to avoid worst-case behavior 468 | ## on already sorted arrays. requires the __compare() and __quicksort() functions. 469 | ## valid values for "how" are: 470 | ## "std asc" 471 | ## use awk's standard rules for comparison, ascending. this is the default 472 | ## "std desc" 473 | ## use awk's standard rules for comparison, descending. 474 | ## "str asc" 475 | ## force comparison as strings, ascending. 476 | ## "str desc" 477 | ## force comparison as strings, descending. 478 | ## "num asc" 479 | ## force a numeric comparison, ascending. 480 | ## "num desc" 481 | ## force a numeric comparison, descending. 482 | ## "val asc" 483 | ## force a numeric comparison of values, ascending. 484 | ## "val desc" 485 | ## force a numeric comparison of values, descending. 486 | function qsorti(array, out, how, values, count, i) { 487 | # make sure how is correct 488 | if (length(how)) { 489 | if (how !~ /^(st[rd]|num|val) (a|de)sc$/) { 490 | return -1; 491 | } 492 | 493 | # how was not passed, use the default 494 | } else { 495 | how = "std asc"; 496 | } 497 | 498 | # sort indexes by value 499 | if (index(how, "val") == 1) { 500 | # loop over each index, and generate two new arrays: the original indices 501 | # mapped to numeric ones, and the values mapped to the same numbers 502 | count = 0; 503 | for (i in array) { 504 | count++; 505 | out[count] = i; 506 | values[count] = array[i]; 507 | } 508 | 509 | # seed the random number generator 510 | srand(); 511 | 512 | # substitute "num" for "val" in "how" 513 | sub(/val/, "num", how); 514 | 515 | # actually sort 516 | __quicksort_val(out, values, 1, count, how); 517 | 518 | # normal index sorting behavior 519 | } else { 520 | # loop over each index, and generate a new array with the original indices 521 | # mapped to new numeric ones 522 | count = 0; 523 | for (i in array) { 524 | out[++count] = i; 525 | } 526 | 527 | # seed the random number generator 528 | srand(); 529 | 530 | # actually sort 531 | __quicksort(out, 1, count, how); 532 | } 533 | 534 | # return the length 535 | return count; 536 | } 537 | 538 | ## usage: rint(number) 539 | ## returns "number" rounded to the nearest integer 540 | function rint(num, n) { 541 | if (num < 0) { 542 | return (num - (n = int(num)) < -.5) ? n - 1 : n; 543 | } else { 544 | return (num - (n = int(num)) >= .5) ? n + 1 : n; 545 | } 546 | } 547 | 548 | ## usage: set_cols(array) 549 | ## sets the following values in "array" with tput. printing them will format 550 | ## any text afterwards. colors and formats are: 551 | ## bold - bold text (can be combined with a color) 552 | ## black - black text 553 | ## red - red text 554 | ## green - green text 555 | ## yellow - yellow text 556 | ## blue - blue text 557 | ## magenta - magenta text 558 | ## cyan - cyan text 559 | ## white - white text 560 | ## reset - resets to default settings 561 | function set_cols(array) { 562 | # bold 563 | cmd = "tput bold"; 564 | cmd | getline array["bold"]; 565 | close(cmd); 566 | # black 567 | cmd = "tput setaf 0"; 568 | cmd | getline array["black"]; 569 | close(cmd); 570 | # red 571 | cmd = "tput setaf 1"; 572 | cmd | getline array["red"]; 573 | close(cmd); 574 | # green 575 | cmd = "tput setaf 2"; 576 | cmd | getline array["green"]; 577 | close(cmd); 578 | # yellow 579 | cmd = "tput setaf 3"; 580 | cmd | getline array["yellow"]; 581 | close(cmd); 582 | # blue 583 | cmd = "tput setaf 4"; 584 | cmd | getline array["blue"]; 585 | close(cmd); 586 | # magenta 587 | cmd = "tput setaf 5"; 588 | cmd | getline array["magenta"]; 589 | close(cmd); 590 | # cyan 591 | cmd = "tput setaf 6"; 592 | cmd | getline array["cyan"]; 593 | close(cmd); 594 | # white 595 | cmd = "tput setaf 7"; 596 | cmd | getline array["white"]; 597 | close(cmd); 598 | # reset 599 | cmd = "tput sgr0"; 600 | cmd | getline array["reset"]; 601 | close(cmd); 602 | } 603 | 604 | ## usage: trim(string) 605 | ## returns "string" with leading and trailing whitespace trimmed 606 | function trim(str) { 607 | gsub(/^[[:blank:]]+|[[:blank:]]+$/, "", str); 608 | 609 | return str; 610 | } 611 | 612 | # prints usage information 613 | function usage() { 614 | printf("%s\n\n%s\n\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n\n", 615 | "Usage: bar_graph -- [OPTIONS] [FILE...]", 616 | "The '--' is required, so AWK itself doesn't read the options.", 617 | "Creates a bar graph of the categories and values in FILE(s) (or the standard", 618 | "input if no files are named, or if a single hyphen (-) is given), reading the", 619 | "first field as the category and the second as the value. If multiple lines", 620 | "with the same category exist, the values are added as each is encountered.", 621 | "Negative values will be treated as zero. The graph width is determined by the", 622 | "terminal width. If the standard input is not a terminal, and --width is not", 623 | "used, the default is 80 characters." \ 624 | ) > "/dev/stderr"; 625 | printf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n", 626 | " Options:", 627 | " -d, --delim PATT Use the AWK style regular expression PATT as the", 628 | " field separator, in the same manner of AWK's FS. If", 629 | " -d is not provided, the default FS is used", 630 | " -t, --title TITLE Add the title TITLE to the graph", 631 | " -b, --bar STRING Use STRING for the bars of the graph, instead of", 632 | " \"=\", repeating it as needed. May use a partial value", 633 | " at the end if STRING is not a single character", 634 | " -n, --nums Use the values (or percentages, if -p is used) as", 635 | " the final part of the bar", 636 | " -p, --percent Graph values as percentages of the largest (largest", 637 | " value being 100%)" \ 638 | ) > "/dev/stderr"; 639 | printf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n", 640 | " -P, --percent-tot Graph values as percentages of the total", 641 | " -s, --sort[=HOW] Sort the categories, according to HOW, if provided.", 642 | " If not provided, the default is "std asc". Values", 643 | " for HOW are explained below", 644 | " -i, --ignorecase Ignore the case of the categories. The case used for", 645 | " the category in the graph will be that of the first", 646 | " encountered in the data", 647 | " -w, --width WIDTH Force a graph width of WIDTH. WIDTH is an int >= 40", 648 | " -l, --cat-label LABEL Label the category axis with LABEL", 649 | " -v, --val-label LABEL Label the value axis with LABEL" \ 650 | ) > "/dev/stderr"; 651 | printf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n\n", 652 | " -S, --stty Use \"stty size\" to determine the terminal width", 653 | " instead of \"tput cols\"", 654 | " -c, --color[=WHEN] Use colored output. WHEN is always, auto, or never", 655 | " -B, --bold[=WHEN] Format the output using boldness. WHEN is always,", 656 | " auto, or never", 657 | " -C, --colors STRING Use the colors in STRING to color the graph for -c.", 658 | " STRING is a list of characters, each representing a", 659 | " color. The colors will be used for each line in", 660 | " order, repeating until all of the categories are", 661 | " exhausted. Valid values are explained below.", 662 | " -h, --help Display this help and exit" \ 663 | ) > "/dev/stderr"; 664 | printf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n\n", 665 | "Values for HOW:", 666 | " \"std asc\"", 667 | " use AWK's standard rules for comparison, ascending. this is the default", 668 | " \"std desc\"", 669 | " use AWK's standard rules for comparison, descending.", 670 | " \"str asc\"", 671 | " force comparison as strings, ascending.", 672 | " \"str desc\"", 673 | " force comparison as strings, descending.", 674 | " \"num asc\"", 675 | " force a numeric comparison, ascending.", 676 | " \"num desc\"", 677 | " force a numeric comparison, descending.", 678 | " \"val asc\"", 679 | " sort by value, instead of category, in the same manner as \"num asc\".", 680 | " \"val desc\"", 681 | " sort by value, instead of category, in the same manner as \"num desc\"." \ 682 | ) > "/dev/stderr"; 683 | printf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n", 684 | "Values for --colors:", 685 | " \"B\": black", 686 | " \"r\": red", 687 | " \"g\": green", 688 | " \"y\": yellow", 689 | " \"b\": blue", 690 | " \"m\": magenta", 691 | " \"c\": cyan", 692 | " \"w\": white", 693 | " \"d\": the default terminal color", 694 | "For example, the default color string is \"rgbycm\"" \ 695 | ) > "/dev/stderr"; 696 | } 697 | 698 | BEGIN { 699 | # initialize variables 700 | title = sort_how = vlabel = clabel = ""; 701 | barstr = "="; 702 | width = 80; 703 | colorstr = "rgbycm"; 704 | hastitle = shownums = useperc = perctotal = sort = icase = userwidth = 0; 705 | has_clabel = has_vlabel = stty = color = bold = err = toexit = 0; 706 | 707 | # map long options to the corresponding short ones 708 | longopts["delim"] = "d"; 709 | longopts["title"] = "t"; 710 | longopts["bar"] = "b"; 711 | longopts["nums"] = "n"; 712 | longopts["percent"] = "p"; 713 | longopts["percent-tot"] = "P"; 714 | longopts["sort"] = "s"; 715 | longopts["ignorecase"] = "i"; 716 | longopts["width"] = "w"; 717 | longopts["cat-label"] = "l"; 718 | longopts["val-label"] = "v"; 719 | longopts["stty"] = "S"; 720 | longopts["color"] = "c"; 721 | longopts["bold"] = "B"; 722 | longopts["colors"] = "C"; 723 | longopts["help"] = "h"; 724 | 725 | # parse the options 726 | while ((opt = getopts("d:t:b:npPsiw:l:v:ScBC:h", longopts)) != -1) { 727 | # -h, --help 728 | if (opt == "h") { 729 | usage(); toexit = 1; exit; 730 | 731 | # -d, --delim PATT 732 | } else if (opt == "d") { 733 | FS = optarg; 734 | 735 | # -t, --title TITLE 736 | } else if (opt == "t") { 737 | title = trim(optarg); hastitle = 1; 738 | 739 | # -b, --bar STRING 740 | } else if (opt == "b") { 741 | barstr = optarg; 742 | 743 | # -n, --nums 744 | } else if (opt == "n") { 745 | shownums = 1; 746 | 747 | # -p, --percent 748 | } else if (opt == "p") { 749 | useperc = 1; perctotal = 0; 750 | 751 | # -P, --percent-tot 752 | } else if (opt == "P") { 753 | perctotal = 1; useperc = 0; 754 | 755 | # -s, --sort[=HOW] 756 | } else if (opt == "s") { 757 | # if HOW is provided: 758 | if (length(optarg)) { 759 | # validate HOW 760 | if (optarg !~ /^(st[rd]|num|val) (a|de)sc$/) { 761 | printf("invalid value for HOW: %s\n", optarg) > "/dev/stderr"; 762 | err = toexit = 1; exit; 763 | } 764 | sort_how = optarg; 765 | 766 | # HOW was not provided 767 | } else { 768 | sort_how = "std asc"; 769 | } 770 | 771 | sort = 1; 772 | 773 | # -i, --ignorecase 774 | } else if (opt == "i") { 775 | icase = 1; 776 | 777 | # -w, --width WIDTH 778 | } else if (opt == "w") { 779 | if (!isint(optarg)) { # || optarg < 40) { 780 | printf("invalid value for WIDTH: %s\n", optarg) > "/dev/stderr"; 781 | err = toexit = 1; exit; 782 | } 783 | width = optarg; userwidth = 1; 784 | 785 | # -l, --cat-label LABEL 786 | } else if (opt == "l") { 787 | clabel = optarg; has_clabel = 1; 788 | 789 | # -v, --val-label LABEL 790 | } else if (opt == "v") { 791 | vlabel = optarg; has_vlabel = 1; 792 | 793 | # -S, --stty 794 | } else if (opt == "S") { 795 | stty = 1; 796 | 797 | # -c, --color[=WHEN] 798 | } else if (opt == "c") { 799 | if (length(optarg)) { 800 | if ((color = check_when(optarg)) < 0) { 801 | printf("invalid value for WHEN: %s\n", optarg) > "/dev/stderr"; 802 | err = toexit = 1; exit; 803 | } 804 | } else { 805 | color = check_when("auto"); 806 | } 807 | 808 | # -B, --bold[=WHEN] 809 | } else if (opt == "B") { 810 | if (length(optarg)) { 811 | if ((bold = check_when(optarg)) < 0) { 812 | printf("invalid value for WHEN: %s\n", optarg) > "/dev/stderr"; 813 | err = toexit = 1; exit; 814 | } 815 | } else { 816 | bold = check_when("auto"); 817 | } 818 | 819 | # -C, --colors STRING 820 | } else if (opt == "C") { 821 | colorstr = optarg; 822 | 823 | # error 824 | } else { 825 | err = toexit = 1; exit; 826 | } 827 | } 828 | 829 | # set colors and bold if needed 830 | if (color || bold) { 831 | # available colors 832 | avail_colors["B"] = "black"; 833 | avail_colors["r"] = "red"; 834 | avail_colors["g"] = "green"; 835 | avail_colors["y"] = "yellow"; 836 | avail_colors["b"] = "blue"; 837 | avail_colors["m"] = "magenta"; 838 | avail_colors["c"] = "cyan"; 839 | avail_colors["w"] = "white"; 840 | avail_colors["d"] = "reset"; 841 | 842 | # set the colors to the "colors" array 843 | set_cols(colors); 844 | 845 | # split the color string into an array of chars 846 | tot_colors = split(colorstr, usecolors, ""); 847 | 848 | # loop over each character 849 | for (i=1; i<=tot_colors; i++) { 850 | # invalid color character 851 | if (!(usecolors[i] in avail_colors)) { 852 | printf("invalid color character: %s\n", usecolors[i]) > "/dev/stderr"; 853 | err = toexit = 1; exit; 854 | } 855 | 856 | # create the array of colors 857 | usecolors[i] = avail_colors[usecolors[i]]; 858 | } 859 | } 860 | } 861 | 862 | 863 | # store values from each line 864 | { 865 | # if case is to be ignored... 866 | if (icase) { 867 | category = trim(tolower($1)); # get the all-lower category 868 | 869 | # if this category hasn't been seen yet, store the original cased version 870 | if (!orig_seen[category]++) { 871 | orig_cats[category] = trim($1); 872 | } 873 | 874 | # case is not ignored, just trim 875 | } else { 876 | category = trim($1); 877 | } 878 | 879 | # if no sorting is to occur, store the original order that each is first seen 880 | if (!sort && !order_seen[category]++) { 881 | categories[++tot_cats] = category; 882 | } 883 | 884 | # actually add the values for the cats as we go 885 | vals[category] += $2; 886 | 887 | # add total for -P 888 | if (perctotal) { 889 | total += $2; 890 | } 891 | } 892 | 893 | END { 894 | # exit prematurely if needed 895 | if (toexit) { 896 | exit err; 897 | } 898 | 899 | # find the max length and value 900 | for (i in vals) { 901 | if ((len = length(i)) > maxI) { 902 | maxI = len; 903 | } 904 | 905 | if (vals[i] > maxL) { 906 | maxL = vals[i]; 907 | } 908 | } 909 | if (has_clabel) { 910 | maxI += 2; 911 | } 912 | 913 | # adjust values, if needed 914 | if (useperc || perctotal || shownums) { 915 | for (i in vals) { 916 | if (useperc) { 917 | vals[i] = sprintf("%.1f", (vals[i] / maxL) * 100); 918 | } else if (perctotal) { 919 | vals[i] = sprintf("%.1f", (vals[i] / total) * 100); 920 | } 921 | 922 | if (shownums) { 923 | vals[i] = sprintf("%.1f", vals[i]); 924 | } 925 | } 926 | 927 | if (useperc || perctotal) { 928 | maxL = 100; 929 | } 930 | } 931 | 932 | # set width, if --width wasn't provided 933 | if (!userwidth) { 934 | if (isatty(1)) { 935 | if (stty) { 936 | cmd = "stty size"; 937 | cmd | getline width; 938 | sub(/.* /, "", width); 939 | 940 | } else { 941 | cmd = "tput cols"; 942 | cmd | getline width; 943 | } 944 | 945 | close(cmd); 946 | 947 | # stdout is not a tty 948 | } else { 949 | width = 80; 950 | } 951 | } 952 | 953 | # determine the divisor to fit the graph within the width 954 | bar_width = width - (maxI + 2); 955 | divisor = maxL / bar_width; 956 | 957 | # print the title if it exists 958 | if (hastitle) { 959 | title = fold(title, " \t\n", width); 960 | len = split(title, t, /\n/); 961 | 962 | if (color || bold) { 963 | printf("%s", colors["bold"]); 964 | } 965 | for (i=1; i<=len; i++) { 966 | print center(trim(t[i])); 967 | } 968 | if (color || bold) { 969 | printf("%s", colors["reset"]); 970 | } 971 | 972 | print ""; 973 | } 974 | 975 | # if array is to be sorted, do so 976 | if (sort) { 977 | tot_cats = qsorti(vals, categories, sort_how); 978 | } 979 | 980 | # determine the total height of the graph, sans title and vlabel 981 | height = tot_cats + 2; 982 | 983 | # if a category label was provided, figure out the difference in length 984 | if (has_clabel) { 985 | # if it's positive, print the excess first 986 | if ((clabel_offset = (clabel_len = length(clabel)) - height) > 0) { 987 | for (i=1; i<=clabel_offset; i++) { 988 | print substr(clabel, i, 1); 989 | } 990 | 991 | # if it's negative, divide it by two to center the label 992 | } else if (clabel_offset <= -2) { 993 | clabel_offset = int((clabel_offset + 2) / 2); 994 | } else { 995 | clabel_offset = 0; 996 | } 997 | } 998 | 999 | # iterate over each category and print 1000 | for (i=1; i<=tot_cats; i++) { 1001 | # print the label, if needed 1002 | if (has_clabel) { 1003 | # if the current row has a part of the label, according to the offset 1004 | if ((o = i + clabel_offset) > 0 && o <= clabel_len) { 1005 | printf("%s ", substr(clabel, o, 1)); 1006 | 1007 | # otherwise, just print a space 1008 | } else { 1009 | printf(" "); 1010 | } 1011 | } 1012 | 1013 | # colors and bold 1014 | if (color) { 1015 | printf("%s", colors[usecolors[((i - 1) % tot_colors) + 1]]); 1016 | } 1017 | if (bold && !(i % 2)) { 1018 | printf("%s", colors["bold"]); 1019 | } 1020 | 1021 | # print the category 1022 | printf("%*s ", maxI - has_clabel * 2, 1023 | icase ? orig_cats[categories[i]] : categories[i]); 1024 | 1025 | if (color || bold) { 1026 | printf("%s", colors["reset"]); 1027 | } 1028 | printf("|"); 1029 | if (color) { 1030 | printf("%s", colors[usecolors[((i - 1) % tot_colors) + 1]]); 1031 | } 1032 | if (bold && !(i % 2)) { 1033 | printf("%s", colors["bold"]); 1034 | } 1035 | 1036 | # print bar for positive values 1037 | if (vals[categories[i]] > 0) { 1038 | # determine the length of the bar 1039 | full_len = rint(vals[categories[i]] / divisor); 1040 | if (shownums) { 1041 | bar_len = full_len - length(vals[categories[i]]) - 1; 1042 | } else { 1043 | bar_len = full_len; 1044 | } 1045 | 1046 | # determine the number of times the bar string is to be repeated 1047 | barstr_len = length(barstr); 1048 | repeats = int(bar_len / barstr_len); 1049 | remainder = bar_len % barstr_len; 1050 | 1051 | # print the whole repeats of the bar string 1052 | for (j=1; j<=repeats; j++) { 1053 | printf("%s", barstr); 1054 | } 1055 | 1056 | # if it's not evenly divisible, print the last partial part 1057 | if (remainder) { 1058 | printf("%s", substr(barstr, 1, remainder)); 1059 | } 1060 | 1061 | # if -n was used, print the numbers at the end 1062 | if (shownums) { 1063 | if (full_len > length(vals[categories[i]])) { 1064 | printf("|%s\n", vals[categories[i]]); 1065 | } else { 1066 | printf("%s\n", vals[categories[i]]); 1067 | } 1068 | 1069 | # -n was not used, just terminate with a newline 1070 | } else { 1071 | print ""; 1072 | } 1073 | 1074 | # just print newline for values <= 0 1075 | } else { 1076 | print ""; 1077 | } 1078 | 1079 | if (color || bold) { 1080 | printf("%s", colors["reset"]); 1081 | } 1082 | } 1083 | 1084 | 1085 | # determine the amount of numbers to be displayed on the axis at the bottom 1086 | axis_max = sprintf("%.1f", maxL); # highest number on axis 1087 | axis_spacing = length(axis_max) + 2; # length of each number and spacing 1088 | scalecount = int(bar_width / axis_spacing); # amount of numbers to print 1089 | 1090 | # if the amount of numbers is higher than the actual highest number... 1091 | if (+scalecount > +axis_max) { 1092 | # use the highest number instead 1093 | scalecount = +axis_max; 1094 | axis_spacing = int(bar_width / scalecount); 1095 | } 1096 | 1097 | # leftover space at the beginning 1098 | deadspace = bar_width - (axis_spacing * scalecount); 1099 | 1100 | # figure out numbers to adjust those on the axis 1101 | axis_mult = (scalecount * axis_spacing) / bar_width; 1102 | axis_adj = axis_max * (1 - axis_mult); 1103 | 1104 | # print axis at bottom 1105 | axis = sprintf("%*s", width - has_clabel * 2, "-"); 1106 | gsub(/ /, "-", axis); 1107 | if (has_clabel) { 1108 | if ((o = tot_cats + 1 + clabel_offset) > 0 && o <= clabel_len) { 1109 | printf("%s ", substr(clabel, o, 1)); 1110 | } else { 1111 | printf("--"); 1112 | } 1113 | } 1114 | print axis; 1115 | 1116 | # print the dead space where the categories were 1117 | if (has_clabel) { 1118 | if ((o = tot_cats + 2 + clabel_offset) > 0 && o <= clabel_len) { 1119 | printf("%s ", substr(clabel, o, 1)); 1120 | } else { 1121 | printf(" "); 1122 | } 1123 | } 1124 | printf("%*s |", maxI - has_clabel * 2, ""); 1125 | 1126 | # print the first number or deadspace at the beginning, if needed 1127 | if (deadspace) { 1128 | if (deadspace > length(adj = sprintf("%.1f", axis_adj))) { 1129 | printf("%*.1f", deadspace, adj); 1130 | } else { 1131 | printf("%*s", deadspace, " "); 1132 | } 1133 | } 1134 | 1135 | # loop over each number to be printed 1136 | for (i=1; i<=scalecount; i++) { 1137 | printf("%*.1f", axis_spacing, (((axis_max / scalecount) * axis_mult) * i) + axis_adj); 1138 | } 1139 | 1140 | print ""; 1141 | 1142 | # print the value label, if needed 1143 | if (has_vlabel) { 1144 | vlabel = fold(vlabel, " \t\n", width); 1145 | len = split(vlabel, v, /\n/); 1146 | 1147 | for (i=1; i<=len; i++) { 1148 | print center(trim(v[i])); 1149 | } 1150 | } 1151 | } 1152 | 1153 | 1154 | 1155 | # Copyright Daniel Mills 1156 | # 1157 | # Permission is hereby granted, free of charge, to any person obtaining a copy 1158 | # of this software and associated documentation files (the "Software"), to 1159 | # deal in the Software without restriction, including without limitation the 1160 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 1161 | # sell copies of the Software, and to permit persons to whom the Software is 1162 | # furnished to do so, subject to the following conditions: 1163 | # 1164 | # The above copyright notice and this permission notice shall be included in 1165 | # all copies or substantial portions of the Software. 1166 | # 1167 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 1168 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 1169 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 1170 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 1171 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 1172 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 1173 | # IN THE SOFTWARE. 1174 | -------------------------------------------------------------------------------- /cfold: -------------------------------------------------------------------------------- 1 | #!/usr/bin/awk -f 2 | 3 | # cfold -- [OPTIONS] [FILE...] 4 | # 5 | # the '--' is required, so AWK itself doesn't read the options 6 | # 7 | # Wraps input lines in each FILE (standard input if not provided), writing to 8 | # standard output. The default width is that of the terminal, or 80 columns if 9 | # standard output is not a terminal. If FILE is '-', also reads the standard 10 | # input 11 | # 12 | # Options: 13 | # -c, --center center each line on the terminal. assumes a width of 80 14 | # columns if standard output is not a terminal 15 | # -b, --break[=LIST] break lines at spaces. LIST, if provided, is a list of 16 | # characters to break lines at instead of spaces. For 17 | # example, --break=abc will break lines on "a" or "b" 18 | # Note: an empty string for LIST will revert to the 19 | # default behavior, it is not the same as omitting -b 20 | # -i, --ignore-breaks convert existing single line breaks to spaces. multiple 21 | # line breaks like those at the end of paragraphs will be 22 | # truncated to a single empty line 23 | # -w, --width WIDTH use WIDTH columns instead of the terminal's width 24 | # -t, --trim trim leading and trailing whitespace from each line 25 | # prior to folding 26 | # -h, --help display this help and exit 27 | 28 | 29 | ## usage: center(string[, width]) 30 | ## returns "string" centered based on "width". if "width" is not provided (or 31 | ## is 0), uses the width of the terminal, or 80 if standard output is not open 32 | ## on a terminal. 33 | ## note: does not check the length of the string. if it's wider than the 34 | ## terminal, it will not center lines other than the first. for best results, 35 | ## combine with fold() (see the cfold script in the examples directory for a 36 | ## script that does exactly this) 37 | function center(str, cols, off, cmd) { 38 | if (!cols) { 39 | # checks if stdout is a tty 40 | if (system("test -t 1")) { 41 | cols = 80; 42 | } else { 43 | cmd = "tput cols"; 44 | cmd | getline cols; 45 | close(cmd); 46 | } 47 | } 48 | 49 | off = int((cols/2) + (length(str)/2)); 50 | 51 | return sprintf("%*s", off, str); 52 | } 53 | 54 | ## usage: fold(string, sep[, width]) 55 | ## returns "string", wrapped, with lines broken on "sep" to "width" columns. 56 | ## "sep" is a list of characters to break at, similar to IFS in a POSIX shell. 57 | ## if "sep" is empty, wraps at exactly "width" characters. if "width" is not 58 | ## provided (or is 0), uses the width of the terminal, or 80 if standard output 59 | ## is not open on a terminal. 60 | ## note: currently, tabs are squeezed to a single space. this will be fixed 61 | function fold(str, sep, cols, out, cmd, i, len, chars, c, last, f, first) { 62 | if (!cols) { 63 | # checks if stdout is a tty 64 | if (system("test -t 1")) { 65 | cols = 80; 66 | } else { 67 | cmd = "tput cols"; 68 | cmd | getline cols; 69 | close(cmd); 70 | } 71 | } 72 | 73 | # squeeze tabs and newlines to spaces 74 | gsub(/[\t\n]/, " ", str); 75 | 76 | # if "sep" is empty, just fold on cols with substr 77 | if (!length(sep)) { 78 | len = length(str); 79 | 80 | out = substr(str, 1, cols); 81 | for (i=cols+1; i<=len; i+=cols) { 82 | out = out "\n" substr(str, i, cols); 83 | } 84 | 85 | return out; 86 | 87 | # otherwise, we have to loop over every character (can't split() on sep, it 88 | # would destroy the existing separators) 89 | } else { 90 | # split string into char array 91 | len = split(str, chars, //); 92 | # set boolean, used to assign the first line differently 93 | first = 1; 94 | 95 | for (i=1; i<=len; i+=last) { 96 | f = 0; 97 | for (c=i+cols-1; c>=i; c--) { 98 | if (index(sep, chars[c])) { 99 | last = c - i + 1; 100 | f = 1; 101 | break; 102 | } 103 | } 104 | 105 | if (!f) { 106 | last = cols; 107 | } 108 | 109 | if (first) { 110 | out = substr(str, i, last); 111 | first = 0; 112 | } else { 113 | out = out "\n" substr(str, i, last); 114 | } 115 | } 116 | } 117 | 118 | # return the output 119 | return out; 120 | } 121 | 122 | ## usage: getopts(optstring [, longopt_array ]) 123 | ## parses options, and deletes them from ARGV. "optstring" is of the form 124 | ## "ab:c". each letter is a possible option. if the letter is followed by a 125 | ## colon (:), then the option requires an argument. if an argument is not 126 | ## provided, or an invalid option is given, getopts will print the appropriate 127 | ## error message and return "?". returns each option as it's read, and -1 when 128 | ## no options are left. "optind" will be set to the index of the next 129 | ## non-option argument when finished. "optarg" will be set to the option's 130 | ## argument, when provided. if not provided, "optarg" will be empty. "optname" 131 | ## will be set to the current option, as provided. getopts will delete each 132 | ## option and argument that it successfully reads, so awk will be able to treat 133 | ## whatever's left as filenames/assignments, as usual. if provided, 134 | ## "longopt_array" is the name of an associative array that maps long options to 135 | ## the appropriate short option. (do not include the hyphens on either). 136 | ## sample usage can be found in the examples dir, with gawk extensions, or in 137 | ## the ogrep script for a POSIX example: https://github.com/e36freak/ogrep 138 | function getopts(optstring, longarr, opt, trimmed, hasarg, repeat) { 139 | hasarg = repeat = 0; 140 | optarg = ""; 141 | # increment optind 142 | optind++; 143 | 144 | # return -1 if the current arg is not an option or there are no args left 145 | if (ARGV[optind] !~ /^-/ || optind >= ARGC) { 146 | return -1; 147 | } 148 | 149 | # if option is "--" (end of options), delete arg and return -1 150 | if (ARGV[optind] == "--") { 151 | for (i=1; i<=optind; i++) { 152 | delete ARGV[i]; 153 | } 154 | return -1; 155 | } 156 | 157 | # if the option is a long argument... 158 | if (ARGV[optind] ~ /^--/) { 159 | # trim hyphens 160 | trimmed = substr(ARGV[optind], 3); 161 | # if of the format --foo=bar, split the two. assign "bar" to optarg and 162 | # set hasarg to 1 163 | if (trimmed ~ /=/) { 164 | optarg = trimmed; 165 | sub(/=.*/, "", trimmed); sub(/^[^=]*=/, "", optarg); 166 | hasarg = 1; 167 | } 168 | 169 | # invalid long opt 170 | if (!(trimmed in longarr)) { 171 | printf("unrecognized option -- '%s'\n", ARGV[optind]) > "/dev/stderr"; 172 | return "?"; 173 | } 174 | 175 | opt = longarr[trimmed]; 176 | # set optname by prepending dashes to the trimmed argument 177 | optname = "--" trimmed; 178 | 179 | # otherwise, it is a short option 180 | } else { 181 | # remove the hyphen, and get just the option letter 182 | opt = substr(ARGV[optind], 2, 1); 183 | # set trimmed to whatevers left 184 | trimmed = substr(ARGV[optind], 3); 185 | 186 | # invalid option 187 | if (!index(optstring, opt)) { 188 | printf("invalid option -- '%s'\n", opt) > "/dev/stderr"; 189 | return "?"; 190 | } 191 | 192 | # if there is more to the argument than just -o 193 | if (length(trimmed)) { 194 | # if option requires an argument, set the rest to optarg and hasarg to 1 195 | if (index(optstring, opt ":")) { 196 | optarg = trimmed; 197 | hasarg = 1; 198 | 199 | # otherwise, prepend a hyphen to the rest and set repeat to 1, so the 200 | # same arg is processed again without the first option 201 | } else { 202 | ARGV[optind] = "-" trimmed; 203 | repeat = 1; 204 | } 205 | } 206 | 207 | # set optname by prepending a hypen to opt 208 | optname = "-" opt; 209 | } 210 | 211 | # if the option requires an arg and hasarg is 0 212 | if (index(optstring, opt ":") && !hasarg) { 213 | # increment optind, check if no arguments are left 214 | if (++optind >= ARGC) { 215 | printf("option requires an argument -- '%s'\n", optname) > "/dev/stderr"; 216 | return "?"; 217 | } 218 | 219 | # set optarg 220 | optarg = ARGV[optind]; 221 | 222 | # if repeat is set, decrement optind so we process the same arg again 223 | # mutually exclusive to needing an argument, otherwise hasarg would be set 224 | } else if (repeat) { 225 | optind--; 226 | } 227 | 228 | # delete all arguments up to this point, just to make sure 229 | for (i=1; i<=optind; i++) { 230 | delete ARGV[i]; 231 | } 232 | 233 | # return the option letter 234 | return opt; 235 | } 236 | 237 | ## usage: trim(string) 238 | ## returns "string" with leading and trailing whitespace trimmed 239 | function trim(str) { 240 | gsub(/^[[:blank:]]+|[[:blank:]]+$/, "", str); 241 | 242 | return str; 243 | } 244 | 245 | # prints usage 246 | function usage() { 247 | printf("%s\n\n%s\n\n%s\n%s\n%s\n%s\n\n", 248 | "cfold -- [OPTIONS] [FILE...]", 249 | "the '--' is required, so AWK itself doesn't read the options", 250 | "Wraps input lines in each FILE (standard input if not provided), writing to", 251 | "standard output. The default width is that of the terminal, or 80 columns if", 252 | "standard output is not a terminal. If FILE is '-', also reads the standard", 253 | "input") > "/dev/stderr"; 254 | printf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n", 255 | " Options:", 256 | " -c, --center center each line on the terminal. assumes a width of 80", 257 | " columns if standard output is not a terminal", 258 | " -b, --break[=LIST] break lines at spaces. LIST, if provided, is a list of", 259 | " characters to break lines at instead of spaces. For", 260 | " example, --break=abc will break lines on \"a\" or \"b\"", 261 | " Note: an empty string for LIST will revert to the", 262 | " default behavior, it is not the same as omitting -b", 263 | " -i, --ignore-breaks convert existing single line breaks to spaces. multiple", 264 | " line breaks like those at the end of paragraphs will be", 265 | " truncated to a single empty line", 266 | " -w, --width WIDTH use WIDTH columns instead of the terminal's width", 267 | " -t, --trim trim leading and trailing whitespace from each line", 268 | " prior to folding", 269 | " -h, --help display this help and exit") > "/dev/stderr"; 270 | } 271 | 272 | BEGIN { 273 | # initialize variables to defaults 274 | toexit = err = 0; 275 | tocenter = toignore = totrim = 0; 276 | break_chars = ""; 277 | 278 | # get default width 279 | if (system("test -t 1")) { 280 | # stdout is not open on a tty 281 | width = 80 282 | } else { 283 | cmd = "tput cols"; 284 | cmd | getline width; 285 | close(cmd); 286 | } 287 | 288 | # map long options to short options 289 | longopts["center"] = "c"; 290 | longopts["break"] = "b"; 291 | longopts["ignore-breaks"] = "i"; 292 | longopts["width"] = "w"; 293 | longopts["trim"] = "t"; 294 | longopts["help"] = "h"; 295 | 296 | # parse the options 297 | while ((opt = getopts("cbiw:th", longopts)) != -1) { 298 | switch(opt) { 299 | # -c, --center 300 | case "c": 301 | tocenter = 1; break; 302 | 303 | # -b, --break 304 | case "b": 305 | if (length(optarg)) { 306 | break_chars = optarg; 307 | } else { 308 | break_chars = " \t\n"; 309 | } 310 | break; 311 | 312 | # -i, --ignore-breaks 313 | case "i": 314 | toignore = 1; break; 315 | 316 | # w, --width 317 | case "w": 318 | # make sure optarg is an integer 319 | if (optarg !~ /^[0-9]+$/) { 320 | printf("'%s' is not a valid argument for '%s', must be a number\n", 321 | optarg, optname) > "/dev/stderr"; 322 | err = toexit = 1; 323 | exit; 324 | } 325 | width = optarg; 326 | break; 327 | 328 | # -t, --trim 329 | case "t": 330 | totrim = 1; break; 331 | 332 | # -h, --help 333 | case "h": 334 | usage(); toexit = 1; exit; 335 | 336 | # error 337 | case "?": 338 | default: 339 | err = toexit = 1; 340 | exit; 341 | } 342 | } 343 | 344 | # if --ignore-breaks was used, set RS to null so that paragraphs are 345 | # treated as one line 346 | if (toignore) { 347 | RS = ""; 348 | } 349 | } 350 | 351 | ######## 352 | 353 | # if --ignore-breaks was used, print extra newline between records 354 | toignore && NR > 1 { 355 | print ""; 356 | } 357 | 358 | # fold each record (line, or paragraph) 359 | { 360 | 361 | # if --trim was used, reassign $0 with leading/trailing whitespace removed 362 | if (totrim) { 363 | $0 = trim($0); 364 | } 365 | 366 | out = fold($0, break_chars, width); 367 | 368 | # if text is to be centered, split out into an array of lines and center each 369 | if (tocenter) { 370 | len = split(out, lines, /\n/); 371 | 372 | for (i=1; i<=len; i++) { 373 | print center(lines[i]); 374 | } 375 | } else { 376 | print out; 377 | } 378 | } 379 | 380 | # exit according to "err" 381 | END { 382 | exit err; 383 | } 384 | 385 | 386 | 387 | # Copyright Daniel Mills 388 | # 389 | # Permission is hereby granted, free of charge, to any person obtaining a copy 390 | # of this software and associated documentation files (the "Software"), to 391 | # deal in the Software without restriction, including without limitation the 392 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 393 | # sell copies of the Software, and to permit persons to whom the Software is 394 | # furnished to do so, subject to the following conditions: 395 | # 396 | # The above copyright notice and this permission notice shall be included in 397 | # all copies or substantial portions of the Software. 398 | # 399 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 400 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 401 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 402 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 403 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 404 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 405 | # IN THE SOFTWARE. 406 | -------------------------------------------------------------------------------- /fah-stats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/awk -f 2 | 3 | function to_sec(date, t) { 4 | split(date, t, /:/); 5 | return((t[1]*3600) + (t[2]*60) + t[3]); 6 | } 7 | 8 | function to_hr(sec, m, s) { 9 | s = sec % 60; 10 | sec = int(sec / 60); 11 | m = sec % 60; 12 | sec = int(sec / 60); 13 | 14 | return(sprintf("%02d:%02d:%02d", sec, m, s)); 15 | } 16 | 17 | BEGIN { 18 | FS = "[(]|%[)][[:blank:]]*$"; 19 | } 20 | 21 | /^\[([0-9]{2}:){2}[0-9]{2}\]/ { 22 | split($0, t, /[][]/); 23 | cur = to_sec(t[2]); 24 | if (cur < lastcur) { 25 | day++; 26 | mod = day * 24 * 60 * 60; 27 | } 28 | lastcur = cur; 29 | time = cur + mod; 30 | } 31 | 32 | NF != 3 { 33 | next; 34 | } 35 | 36 | !$2 { 37 | wu++; 38 | } 39 | 40 | { 41 | wu = wu ? wu : 1; 42 | if (!(wu in start)) { 43 | start[wu] = $2; 44 | startt[wu] = time; 45 | } 46 | if ($2 == 100) { 47 | end[wu] = last; 48 | endt[wu] = time; 49 | } 50 | 51 | steps[wu,$2] = $0; 52 | times[wu,$2] = time; 53 | 54 | last = $2; 55 | } 56 | 57 | END { 58 | if (FILENAME != "-") { 59 | cmd = "stat -c %Y '" FILENAME "'"; 60 | cmd | getline mtime; 61 | close(cmd); 62 | 63 | mod = (systime() - mtime); 64 | time = time + mod; 65 | } 66 | 67 | if (NF == 3) { 68 | end[wu] = last; 69 | 70 | for (i=start[wu]+1; i<=last; i++) { 71 | tot += times[wu,i] - times[wu,i-1]; 72 | } 73 | 74 | printf("\nThis WU:\n"); 75 | printf("%30s: %02d%%\n", "Current Progress", last); 76 | 77 | if (last > 0) { 78 | avg = int(tot/last); 79 | } else { 80 | avg = time - startt[wu]; 81 | } 82 | printf("%30s: %s\n", "AVG time per percent", to_hr(avg)); 83 | 84 | diff = time - startt[wu]; 85 | printf("%30s: %s\n", "Time spent on current WU", to_hr(diff)); 86 | 87 | rem = 100 - last; 88 | printf("%30s: %s\n", "ETA to completion", to_hr((avg*rem)-mod)); 89 | 90 | fin = endt[wu-1]; 91 | } else { 92 | fin = endt[wu]; 93 | printf("No WU currently in progress\n"); 94 | } 95 | printf("\n"); 96 | 97 | if (wu > 1) { 98 | perc = ftot = wtot = wus = 0; 99 | 100 | for (i=1; i<=wu; i++) { 101 | if (!end[i]) { 102 | continue; 103 | } 104 | tot = 0; 105 | 106 | for (j=start[i]+1; j<=end[i]; j++) { 107 | tot += times[i,j] - times[i,j-1]; 108 | perc++; 109 | } 110 | 111 | wuperc = (end[i] - start[i]) / 100; 112 | wtot += int(tot*(1/wuperc)); 113 | ftot += tot; 114 | wus++; 115 | } 116 | 117 | printf("\nAll WUs in the log:\n"); 118 | 119 | avg = int(ftot/perc); 120 | printf("%30s: %s\n", "AVG time per percent", to_hr(avg)); 121 | 122 | avg = int(wtot/wus); 123 | printf("%30s: %s\n", "AVG time to complete WU", to_hr(avg)); 124 | 125 | diff = time - fin; 126 | printf("%30s: %s\n", "Time since last completed WU", to_hr(diff)); 127 | } else { 128 | printf("No previous WUs in the log\n"); 129 | } 130 | printf("\n"); 131 | } 132 | 133 | 134 | 135 | # Copyright Daniel Mills 136 | # 137 | # Permission is hereby granted, free of charge, to any person obtaining a copy 138 | # of this software and associated documentation files (the "Software"), to 139 | # deal in the Software without restriction, including without limitation the 140 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 141 | # sell copies of the Software, and to permit persons to whom the Software is 142 | # furnished to do so, subject to the following conditions: 143 | # 144 | # The above copyright notice and this permission notice shall be included in 145 | # all copies or substantial portions of the Software. 146 | # 147 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 148 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 149 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 150 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 151 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 152 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 153 | # IN THE SOFTWARE. 154 | -------------------------------------------------------------------------------- /fib.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | int main(int argc, char *argv[]) { 6 | mpz_t i, m, t; 7 | mpz_init_set_si(i, 2); 8 | mpz_init(t); 9 | mpz_init_set_str(m, argv[argc-1], 10); 10 | mpz_t last, cur; 11 | mpz_init_set_si(last, 1); 12 | mpz_init_set_si(cur, 1); 13 | mpz_t temp; 14 | mpz_init(temp); 15 | 16 | while (mpz_cmp(i, m) <= 0) { 17 | mpz_add_ui(i, i, 1); 18 | mpz_set(temp, cur); 19 | mpz_add(cur, last, cur); 20 | mpz_set(last, temp); 21 | } 22 | 23 | gmp_printf("%Zd\n", last); 24 | return 0; 25 | } 26 | 27 | 28 | 29 | /* 30 | Copyright Daniel Mills 31 | 32 | Permission is hereby granted, free of charge, to any person obtaining a copy 33 | of this software and associated documentation files (the "Software"), to 34 | deal in the Software without restriction, including without limitation the 35 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 36 | sell copies of the Software, and to permit persons to whom the Software is 37 | furnished to do so, subject to the following conditions: 38 | 39 | The above copyright notice and this permission notice shall be included in 40 | all copies or substantial portions of the Software. 41 | 42 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 43 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 44 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 45 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 46 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 47 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 48 | IN THE SOFTWARE. 49 | */ 50 | -------------------------------------------------------------------------------- /flatten: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | shopt -s extglob 4 | 5 | # default behavior is to print confirmation prompt 6 | confirm=1 7 | 8 | # print error message and exit 9 | die() { 10 | printf '%s\n' "$@" >&2 11 | exit 1 12 | } 13 | 14 | # adds '.N' to a filename, sans extension, until it doesn't exist 15 | new_filename() { 16 | local out=$1 i=0 17 | 18 | if [[ $out = *+([^/]).*([^/]) ]]; then 19 | local base=${1%.*} ext=${1##*.} 20 | 21 | while [[ -e $out ]]; do 22 | out=$base.$((++i)).$ext 23 | done 24 | else 25 | while [[ -e $out ]]; do 26 | out=$1.$((++i)) 27 | done 28 | fi 29 | 30 | printf '%s\n' "$out" 31 | } 32 | 33 | usage() { 34 | cat <<'EOF' 35 | flatten [OPTIONS] [DIR] 36 | 37 | Recursively moves all files in sub-directories to DIR (the current working 38 | directory if not provided), and deletes the empty sub-directories. Treats 39 | symbolic links as normal files, creating a new symlink in DIR. 40 | 41 | Options: 42 | -h, --help Display this help and exit 43 | -n, --no-confirm Do not display a confirmation prompt 44 | EOF 45 | } 46 | 47 | # iterate over options, breaking -ab into -a -b and --foo=bar into --foo bar 48 | # also turns -- into --endopts to avoid issues with things like wget -O- 49 | while (($#)); do 50 | case $1 in 51 | # if option is of type -ab 52 | -[!-]?*) 53 | # loop over each character starting with the second 54 | for ((i=1; i<${#1}; i++)); do 55 | # add current char to options 56 | options+=("-${1:i:1}") 57 | done 58 | ;; 59 | # end of options, stop breaking them up 60 | --) 61 | options+=(--endopts) 62 | shift 63 | options+=("$@") 64 | break 65 | ;; 66 | # otherwise, nothing special 67 | *) options+=("$1");; 68 | esac 69 | 70 | shift 71 | done 72 | # set new positional parameters to altered options 73 | set -- "${options[@]}" 74 | unset options 75 | 76 | # actually parse the options and do stuff 77 | while [[ $1 = -?* ]]; do 78 | case $1 in 79 | -h|--help) usage >&2; exit 0;; 80 | -n|--no-confirm) confirm=0;; 81 | --endopts) shift; break;; 82 | *) die "invalid option: $1";; 83 | esac 84 | 85 | shift 86 | done 87 | 88 | dir=${1:-"$PWD"} 89 | 90 | if ((confirm)); then 91 | read -p "Flattening $dir, continue? [y/N] " reply >/dev/tty 92 | 93 | if [[ ${reply,,} != y?(es) ]]; then 94 | exit 95 | fi 96 | fi 97 | 98 | # iterate over each top level dir 99 | for d in "$dir"/*/; do 100 | # skip symlinks 101 | [[ -L ${d%/} ]] && continue 102 | 103 | # iterate over each file in the dir recursively, depth first 104 | while IFS= read -rd '' f; do 105 | bname=${f##*/} 106 | 107 | # if it's a symlink, use readlink to get the target and create a new one 108 | if [[ -L $f ]]; then 109 | if [[ -e $f ]]; then 110 | if ! link_target=$(readlink -e "$f"); then 111 | die "error getting the target of symlink $f" 112 | fi 113 | 114 | if ! ln -s "$link_target" "$(new_filename "$dir/$bname")"; then 115 | die "error creating new symlink to $link_target" 116 | fi 117 | 118 | # dangling link 119 | else 120 | printf 'symlink %s is dangling, removing' "$f" >&2 121 | fi 122 | 123 | if ! rm -f "$f"; then 124 | die "error removing symlink $f" 125 | fi 126 | 127 | # if it's a dir, remove it (it should be empty) 128 | elif [[ -d $f ]]; then 129 | if ! rmdir "$f"; then 130 | die "$f is not empty, cannot remove" 131 | fi 132 | 133 | # otherwise just move it to the parent 134 | else 135 | if ! mv "$f" "$(new_filename "$dir/$bname")"; then 136 | die "error moving $f" 137 | fi 138 | fi 139 | done < <(find "$d" -depth -print0) 140 | done 141 | 142 | 143 | 144 | # Copyright Daniel Mills 145 | # 146 | # Permission is hereby granted, free of charge, to any person obtaining a copy 147 | # of this software and associated documentation files (the "Software"), to 148 | # deal in the Software without restriction, including without limitation the 149 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 150 | # sell copies of the Software, and to permit persons to whom the Software is 151 | # furnished to do so, subject to the following conditions: 152 | # 153 | # The above copyright notice and this permission notice shall be included in 154 | # all copies or substantial portions of the Software. 155 | # 156 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 157 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 158 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 159 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 160 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 161 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 162 | # IN THE SOFTWARE. 163 | -------------------------------------------------------------------------------- /getmtime.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | /* usage: rep_filename STRING CHAR FILENAME 12 | replaces a %c specifier with a filename (only the first). CHAR is 'c' 13 | returns the result */ 14 | char *rep_filename(char *str; char *tok, char *fn) { 15 | 16 | } 17 | 18 | /* fixed string replacement. 19 | usage: replace_str STRING SEARCH REP 20 | returns the result */ 21 | char *replace_str(char *str, char *orig, char *rep) { 22 | static char buffer[4096]; 23 | char *p; 24 | 25 | /* Is 'orig' even in 'str'? */ 26 | if (!(p = strstr(str, orig))) { 27 | return str; 28 | } 29 | 30 | /* Copy characters from 'str' start to 'orig' st$ */ 31 | strncpy(buffer, str, p-str); 32 | buffer[p-str] = '\0'; 33 | 34 | sprintf(buffer + (p - str), "%s%s", rep, p + strlen(orig)); 35 | 36 | return buffer; 37 | } 38 | 39 | int main(int argc, char *argv[]) { 40 | static char mtime[512]; 41 | struct stat st; 42 | int i; 43 | 44 | /* make sure enough args are given. if not, print usage */ 45 | if (argc < 3) { 46 | fprintf(stderr, "usage: %s FORMAT FILE [...]\n\n", argv[0]); 47 | fprintf(stderr, "prints the mtime for each FILE given according to FORMAT\n"); 48 | fprintf(stderr, "FORMAT is any string valid for strftime(3)\n"); 49 | fprintf(stderr, "%%N may also be used for the filename\n"); 50 | /* note: %%N will have issues, it's just a literal replacement */ 51 | 52 | exit(EXIT_FAILURE); 53 | } 54 | 55 | /* loop over each FILE */ 56 | for (i=2; i 81 | 82 | Permission is hereby granted, free of charge, to any person obtaining a copy 83 | of this software and associated documentation files (the "Software"), to 84 | deal in the Software without restriction, including without limitation the 85 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 86 | sell copies of the Software, and to permit persons to whom the Software is 87 | furnished to do so, subject to the following conditions: 88 | 89 | The above copyright notice and this permission notice shall be included in 90 | all copies or substantial portions of the Software. 91 | 92 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 93 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 94 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 95 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 96 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 97 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 98 | IN THE SOFTWARE. 99 | */ 100 | -------------------------------------------------------------------------------- /lessopen: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # this script is for the LESSOPEN env var, and is an "input pipe". it allows 4 | # less to read archives, pdfs (with pdftotext), and will use lynx -dump for html 5 | # to use it, set LESSOPEN='|lessopen %s' 6 | exec 2>/dev/null 7 | 8 | shopt -s nocasematch 9 | 10 | if [[ $1 = *.tar* ]]; then 11 | if [[ $1 = *z ]]; then 12 | tar tz "$1" 13 | else 14 | tar t "$1" 15 | fi 16 | 17 | return 18 | fi 19 | 20 | case "$1" in 21 | *.gz) zcat "$1";; 22 | *.xz) xzcat "$1";; 23 | *.zip) unzip -p "$1";; 24 | *.pdf) pdftotext -layout -nopgbrk "$1" -;; 25 | *.html) lynx -dump "$1";; 26 | *) cat "$1";; 27 | esac 28 | 29 | 30 | 31 | # Copyright Daniel Mills 32 | # 33 | # Permission is hereby granted, free of charge, to any person obtaining a copy 34 | # of this software and associated documentation files (the "Software"), to 35 | # deal in the Software without restriction, including without limitation the 36 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 37 | # sell copies of the Software, and to permit persons to whom the Software is 38 | # furnished to do so, subject to the following conditions: 39 | # 40 | # The above copyright notice and this permission notice shall be included in 41 | # all copies or substantial portions of the Software. 42 | # 43 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 44 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 45 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 46 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 47 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 48 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 49 | # IN THE SOFTWARE. 50 | -------------------------------------------------------------------------------- /merge: -------------------------------------------------------------------------------- 1 | #!/bin/awk -f 2 | 3 | # usage: merge -- [OPTIONS] [FILE ...] 4 | # 5 | # The '--' is required, so AWK itself doesn't read the options 6 | # 7 | # Merge sorted FILEs into one, and print the result to the standard output. 8 | # 9 | # Options: 10 | # -h, --help Display this help and exit 11 | 12 | 13 | 14 | ## usage: getopts(optstring [, longopt_array ]) 15 | ## Parses options, and deletes them from ARGV. "optstring" is of the form 16 | ## "ab:c". Each letter is a possible option. If the letter is followed by a 17 | ## colon (:), then the option requires an argument. If an argument is not 18 | ## provided, or an invalid option is given, getopts will print the appropriate 19 | ## error message and return "?". Returns each option as it's read, and -1 when 20 | ## no options are left. "optind" will be set to the index of the next 21 | ## non-option argument when finished. "optarg" will be set to the option's 22 | ## argument, when provided. If not provided, "optarg" will be empty. "optname" 23 | ## will be set to the current option, as provided. Getopts will delete each 24 | ## option and argument that it successfully reads, so awk will be able to treat 25 | ## whatever's left as filenames/assignments, as usual. If provided, 26 | ## "longopt_array" is the name of an associative array that maps long options to 27 | ## the appropriate short option (do not include the hyphens on either). 28 | ## Sample usage can be found in the examples dir, with gawk extensions, or in 29 | ## the ogrep script for a POSIX example: https://github.com/e36freak/ogrep 30 | function getopts(optstring, longarr, opt, trimmed, hasarg, repeat) { 31 | hasarg = repeat = 0; 32 | optarg = ""; 33 | # increment optind 34 | optind++; 35 | 36 | # return -1 if the current arg is not an option or there are no args left 37 | if (ARGV[optind] !~ /^-/ || optind >= ARGC) { 38 | return -1; 39 | } 40 | 41 | # if option is "--" (end of options), delete arg and return -1 42 | if (ARGV[optind] == "--") { 43 | for (i=1; i<=optind; i++) { 44 | delete ARGV[i]; 45 | } 46 | return -1; 47 | } 48 | 49 | # if the option is a long argument... 50 | if (ARGV[optind] ~ /^--/) { 51 | # trim hyphens 52 | trimmed = substr(ARGV[optind], 3); 53 | # if of the format --foo=bar, split the two. assign "bar" to optarg and 54 | # set hasarg to 1 55 | if (trimmed ~ /.*=.*/) { 56 | optarg = trimmed; 57 | sub(/=.*/, "", trimmed); sub(/^[^=]*=/, "", optarg); 58 | hasarg = 1; 59 | } 60 | 61 | # invalid long opt 62 | if (!(trimmed in longarr)) { 63 | printf("unrecognized option -- '%s'\n", ARGV[optind]) > "/dev/stderr"; 64 | return "?"; 65 | } 66 | 67 | opt = longarr[trimmed]; 68 | # set optname by prepending dashes to the trimmed argument 69 | optname = "--" trimmed; 70 | 71 | # otherwise, it is a short option 72 | } else { 73 | # remove the hyphen, and get just the option letter 74 | opt = substr(ARGV[optind], 2, 1); 75 | # set trimmed to whatevers left 76 | trimmed = substr(ARGV[optind], 3); 77 | 78 | # invalid option 79 | if (!index(optstring, opt)) { 80 | printf("invalid option -- '%s'\n", opt) > "/dev/stderr"; 81 | return "?"; 82 | } 83 | 84 | # if there is more to the argument than just -o 85 | if (length(trimmed)) { 86 | # if option requires an argument, set the rest to optarg and hasarg to 1 87 | if (index(optstring, opt ":")) { 88 | optarg = trimmed; 89 | hasarg = 1; 90 | 91 | # otherwise, prepend a hyphen to the rest and set repeat to 1, so the 92 | # same arg is processed again without the first option 93 | } else { 94 | ARGV[optind] = "-" trimmed; 95 | repeat = 1; 96 | } 97 | } 98 | 99 | # set optname by prepending a hypen to opt 100 | optname = "-" opt; 101 | } 102 | 103 | # if the option requires an arg and hasarg is 0 104 | if (index(optstring, opt ":") && !hasarg) { 105 | # increment optind, check if no arguments are left 106 | if (++optind >= ARGC) { 107 | printf("option requires an argument -- '%s'\n", optname) > "/dev/stderr"; 108 | return "?"; 109 | } 110 | 111 | # set optarg 112 | optarg = ARGV[optind]; 113 | 114 | # if repeat is set, decrement optind so we process the same arg again 115 | # mutually exclusive to needing an argument, otherwise hasarg would be set 116 | } else if (repeat) { 117 | optind--; 118 | } 119 | 120 | # delete all arguments up to this point, just to make sure 121 | for (i=1; i<=optind; i++) { 122 | delete ARGV[i]; 123 | } 124 | 125 | # return the option letter 126 | return opt; 127 | } 128 | 129 | # print the usage information 130 | function usage() { 131 | printf("%s\n\n%s\n\n%s\n\n%s\n%s\n", 132 | "usage: merge -- [OPTIONS] [FILE ...]", 133 | "The '--' is required, so AWK itself doesn't read the options", 134 | "Merge sorted FILEs into one, and print the result to the standard output.", 135 | " Options:", 136 | " -h, --help Display this help and exit" \ 137 | ) > "/dev/stderr"; 138 | } 139 | 140 | BEGIN { 141 | # map long options to short ones 142 | longopts["help"] = "h"; 143 | 144 | # read in the options 145 | while ((opt = getopts("h", longopts)) != -1) { 146 | # -h, --help 147 | if (opt == "h") { 148 | usage(); exit; 149 | 150 | # error 151 | } else { 152 | exit 1; 153 | } 154 | } 155 | 156 | # make sure there are filenames 157 | if (optind >= ARGC) { 158 | printf("no input files given\n") > "/dev/stderr"; 159 | exit 1; 160 | } 161 | 162 | # create array of input files 163 | tot_files = 0; 164 | for (arg=optind; arg 0) { 171 | lines[file] = $0; 172 | } else { 173 | finished[file] = 1; 174 | } 175 | } 176 | 177 | # loop continuously 178 | while (1) { 179 | # make sure there is at least one unfinished file 180 | unfinished = 0; 181 | 182 | # set the highest initial line as the first line left 183 | for (file=1; file<=tot_files; file++) { 184 | if (!finished[file]) { 185 | first = lines[file]; 186 | first_file = file; 187 | 188 | unfinished = 1; 189 | break; 190 | } 191 | } 192 | 193 | # if there are no files left, break the loop 194 | if (!unfinished) { 195 | break; 196 | } 197 | 198 | # loop over each input file, find the highest sorted line 199 | for (file=1; file<=tot_files; file++) { 200 | if (!finished[file] && lines[file] < first) { 201 | first = lines[file]; 202 | first_file = file; 203 | } 204 | } 205 | 206 | # print the highest sorted line 207 | print first; 208 | 209 | # get the next line from that file 210 | if ((getline < files[first_file]) > 0) { 211 | lines[first_file] = $0; 212 | 213 | # no more lines for that file 214 | } else { 215 | finished[first_file] = 1; 216 | } 217 | } 218 | } 219 | 220 | 221 | 222 | # Copyright Daniel Mills 223 | # 224 | # Permission is hereby granted, free of charge, to any person obtaining a copy 225 | # of this software and associated documentation files (the "Software"), to 226 | # deal in the Software without restriction, including without limitation the 227 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 228 | # sell copies of the Software, and to permit persons to whom the Software is 229 | # furnished to do so, subject to the following conditions: 230 | # 231 | # The above copyright notice and this permission notice shall be included in 232 | # all copies or substantial portions of the Software. 233 | # 234 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 235 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 236 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 237 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 238 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 239 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 240 | # IN THE SOFTWARE. 241 | -------------------------------------------------------------------------------- /mycp: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | shopt -s nullglob extglob 4 | 5 | noclobber() { 6 | local out=$1 i=0 7 | 8 | if [[ $out = *+([^/]).*([^/]) ]]; then 9 | local base=${1%.*} ext=${1##*.} 10 | 11 | while [[ -e $out ]]; do 12 | out=$base.$((++i)).$ext 13 | done 14 | else 15 | while [[ -e $out ]]; do 16 | out=$1.$((++i)) 17 | done 18 | fi 19 | 20 | printf '%s\n' "$new" 21 | } 22 | 23 | files=("${@:1:$#-1}") 24 | target="${!#}" 25 | 26 | if [[ -d $target ]]; then 27 | for f in "${files[@]}"; do 28 | [[ -e $f ]] || { 29 | echo "$f does not exist, skipping" >&2 30 | continue 31 | } 32 | 33 | cp "$f" "$(noclobber "$target/$f")" || { 34 | echo "error copying $f to $target/$f, skipping" >&2 35 | continue 36 | } 37 | done 38 | else 39 | for f in "${files[@]}"; do 40 | [[ -e $f ]] || { 41 | echo "$f does not exist, skipping" >&2 42 | continue 43 | } 44 | 45 | cp "$f" "$(noclobber "$target")" || { 46 | echo "error copying $f to $target/$f, skipping" >&2 47 | continue 48 | } 49 | done 50 | fi 51 | 52 | 53 | 54 | # Copyright Daniel Mills 55 | # 56 | # Permission is hereby granted, free of charge, to any person obtaining a copy 57 | # of this software and associated documentation files (the "Software"), to 58 | # deal in the Software without restriction, including without limitation the 59 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 60 | # sell copies of the Software, and to permit persons to whom the Software is 61 | # furnished to do so, subject to the following conditions: 62 | # 63 | # The above copyright notice and this permission notice shall be included in 64 | # all copies or substantial portions of the Software. 65 | # 66 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 67 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 68 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 69 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 70 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 71 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 72 | # IN THE SOFTWARE. 73 | -------------------------------------------------------------------------------- /myunlink.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | int main(int argc, char *argv[]) { 7 | int i, err; 8 | err = 0; 9 | 10 | if (argc < 2) { 11 | fprintf(stderr, "%s\n", "no filenames provided"); 12 | return 2; 13 | } 14 | 15 | for (i=0; i 29 | 30 | Permission is hereby granted, free of charge, to any person obtaining a copy 31 | of this software and associated documentation files (the "Software"), to 32 | deal in the Software without restriction, including without limitation the 33 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 34 | sell copies of the Software, and to permit persons to whom the Software is 35 | furnished to do so, subject to the following conditions: 36 | 37 | The above copyright notice and this permission notice shall be included in 38 | all copies or substantial portions of the Software. 39 | 40 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 41 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 42 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 43 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 44 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 45 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 46 | IN THE SOFTWARE. 47 | */ 48 | -------------------------------------------------------------------------------- /newscript: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | shopt -s extglob nullglob 4 | 5 | nc=0 6 | cron=0 7 | edit=0 8 | fullshebang=0 9 | lang=bash 10 | user_editor=0 11 | editor= 12 | toedit=() 13 | names=() 14 | 15 | # add a PATH assignment and export to the script, based on the curent PATH 16 | # splits on colons so that the lines are truncated to 80 chars 17 | add_path() { 18 | local q=\' IFS=: cur=0 path len i elements 19 | 20 | # split PATH (with escaped single quotes) into array on ":" 21 | IFS=: read -ra path <<<"${PATH//$q/\\$q}" 22 | elements=${#path[@]} 23 | 24 | # iterate over array elements, adding the lengths until the line is too long 25 | for ((i=0, len=0; i<=elements && len<=74; i++)); do 26 | ((len += ${#path[i]} + 1)) 27 | done 28 | ((i--)) 29 | 30 | # if a single element is too long, use it anyway 31 | if ((cur < elements && cur == i)); then 32 | ((i++)) 33 | fi 34 | 35 | # print the assignment, with all of the elements up to "i" 36 | printf "PATH='%s'\n" "${path[*]:cur:i-cur}" 37 | # cur is the element at which the line becomes too long 38 | cur=$i 39 | 40 | # continue looping over the indices, doing the same, until PATH is exhausted 41 | until ((cur >= elements)); do 42 | for ((i=cur, len=0; len<=72 && i<=elements; i++)); do 43 | ((len += ${#path[i]} + 1)) 44 | done 45 | ((i--)) 46 | 47 | if ((cur < elements && cur == i)); then 48 | ((i++)) 49 | fi 50 | 51 | printf "PATH+=':%s'\n" "${path[*]:cur:i-cur}" 52 | cur=$i 53 | done 54 | 55 | # print the export line 56 | printf "%s\n\n" 'export PATH' 57 | } 58 | 59 | # usage: die MESSAGE 60 | # print error and exit 61 | die() { 62 | printf '%s\n' "$@" >&2 63 | exit 1 64 | } 65 | 66 | # usage: err MESSAGE 67 | # print error, do not exit 68 | err() { 69 | printf '%s\n' "$@" >&2 70 | } 71 | 72 | # usage: editor FILE ... 73 | # exec editor (if user supplied), VISUAL, EDITOR, or "vi", in that order 74 | editor() { 75 | if ((user_editor)); then 76 | exec $editor "$@" 77 | elif [[ $VISUAL ]]; then 78 | exec $VISUAL "$@" 79 | else 80 | exec ${EDITOR:-vi} "$@" 81 | fi 82 | } 83 | 84 | # usage: get_names IS_NUL 85 | # reads stdin, appending each name within to the targets array. if IS_NUL is 86 | # unset or empty, reads one name per line. otherwise, reads NUL-delimited names 87 | get_names() { 88 | local cmd=(read -r) name 89 | 90 | if [[ $1 ]]; then 91 | cmd+=(-d '') 92 | fi 93 | 94 | while IFS= "${cmd[@]}" name; do 95 | names+=("$name") 96 | done 97 | } 98 | 99 | # usage: noclobber FILE 100 | # adds '.N' to a filename, sans extension, until it doesn't exist 101 | noclobber() { 102 | local out=$1 i=0 103 | 104 | if [[ $1 = *+([^/]).*([^/]) ]]; then 105 | local base=${1%.*} ext=${1##*.} 106 | 107 | while [[ -e $out ]]; do 108 | out=$base.$((++i)).$ext 109 | done 110 | else 111 | while [[ -e $out ]]; do 112 | out=$1.$((++i)) 113 | done 114 | fi 115 | 116 | printf '%s\n' "$out" 117 | } 118 | 119 | # usage: usage 120 | # ...heh 121 | usage() { 122 | cat <<'EOF' 123 | newscript [OPTIONS] NAME [...] 124 | 125 | Creates a new script for each NAME, in the current working directory 126 | 127 | Options: 128 | -h, --help display this help and exit 129 | -l, --language LANG specify the language LANG, default is bash 130 | -s, --shebang SHEBANG provide the entire shebang as an arg, SHEBANG 131 | -c, --cron add a your current PATH to the beginning of the 132 | script. only available with -l bash (default), or sh 133 | -n, --no-clobber prepends a number to the end of each existing NAME 134 | instead of erring 135 | -e, --edit open created files in a text editor 136 | -p, --editor PROG use PROG instead of the default editor. by default, 137 | VISUAL, EDITOR, or vi will be used, in that order. 138 | implies -e (--edit) 139 | -t, --files-from FILE get a list of NAMEs from FILE, one per line (if NAMEs 140 | are also provided, both will be used) 141 | -T, --files-from0 FILE same as --files-from, but uses NUL-delimited NAMEs 142 | 143 | Available languages for LANG are: 144 | bash, sh, awk, sed, perl, python 145 | EOF 146 | } 147 | 148 | # iterate over options breaking -ab into -a -b when needed and 149 | # --foo=bar into --foo bar 150 | optstring=hl:s:cnep:t:T: 151 | unset options 152 | while (($#)); do 153 | case $1 in 154 | # if option is of type -ab 155 | -[!-]?*) 156 | # loop over each character starting with the second 157 | for ((i=1; i<${#1}; i++)); do 158 | c=${1:i:1} 159 | 160 | # add current char to options 161 | options+=("-$c") 162 | 163 | # if option takes a required argument, and it's not the last char 164 | # make the rest of the string its argument 165 | if [[ $optstring = *"$c:"* && ${1:i+1} ]]; then 166 | options+=("${1:i+1}") 167 | break 168 | fi 169 | done 170 | ;; 171 | # if option is of type --foo=bar, split on first '=' 172 | --?*=*) options+=("${1%%=*}" "${1#*=}");; 173 | # end of options, stop breaking them up 174 | --) 175 | options+=(--endopts) 176 | shift 177 | options+=("$@") 178 | break 179 | ;; 180 | # otherwise, nothing special 181 | *) options+=("$1");; 182 | esac 183 | 184 | shift 185 | done 186 | # set new positional parameters to altered options 187 | set -- "${options[@]}" 188 | unset options 189 | 190 | # actually read the options and set stuff 191 | while [[ $1 = -?* ]]; do 192 | case $1 in 193 | -h|--help) usage >&2; exit 0;; 194 | -c|--cron) cron=1;; 195 | -n|--no-clobber) nc=1;; 196 | -l|--language) 197 | [[ $2 ]] || die "LANG required for $1" 198 | lang=$2 199 | shift 200 | ;; 201 | -s|--shebang) 202 | [[ $2 ]] || die "SHEBANG required for $1" 203 | fullshebang=1 204 | shebang=$2 205 | shift 206 | ;; 207 | -e|--edit) edit=1;; 208 | -p|--editor) 209 | [[ $2 ]] || die "PROG required for $1" 210 | user_editor=1 211 | edit=1 212 | editor=$2 213 | ;; 214 | -t|--files-from) 215 | [[ $2 ]] || die "FILE required for $1" 216 | if [[ $2 = - ]]; then 217 | get_names 218 | elif [[ ! -f $2 || ! -r $2 ]]; then 219 | die "$2 does not exist or is not readable" 220 | else 221 | get_names <"$2" 222 | fi 223 | shift 224 | ;; 225 | -T|--files-from0) 226 | [[ $2 ]] || die "FILE required for $1" 227 | if [[ $2 = - ]]; then 228 | get_names nul 229 | elif [[ ! -f $2 || ! -r $2 ]]; then 230 | die "$2 does not exist or is not readable" 231 | else 232 | get_names nul <"$2" 233 | fi 234 | shift 235 | ;; 236 | --endopts) shift; break;; 237 | *) die "invalid option: $1";; 238 | esac 239 | 240 | shift 241 | done 242 | 243 | # append remaining positional parameters to the names array 244 | names+=("$@") 245 | 246 | # if no names have been passed, either from the command line or if the 247 | # --files-from file was empty, die 248 | if ((!${#names[@]})); then 249 | die "no targets specified" 250 | fi 251 | 252 | # if -s is used, check if the language is bash, sh, or perl, and set lang 253 | if ((fullshebang)); then 254 | case $shebang in 255 | */bash*([!/]) ) lang=bash;; 256 | */sh*([!/]) ) lang=sh;; 257 | */perl*([!/]) ) lang=perl;; 258 | esac 259 | 260 | # otherwise, validate lang and set the correct shebang 261 | else 262 | case $lang in 263 | bash) shebang='#!/bin/bash';; 264 | sh) shebang='#!/bin/sh';; 265 | awk) shebang='#!/usr/bin/awk -f';; 266 | sed) shebang='#!/usr/bin/sed -f';; 267 | perl) shebang='#!/usr/bin/perl';; 268 | python) shebang='#!/usr/bin/python';; 269 | *) die "$lang is not an available preset language" 270 | esac 271 | fi 272 | 273 | # make sure if cron is provided, language is bash or sh 274 | if ((cron)) && [[ $lang != ?(ba)sh ]]; then 275 | die "cron option is only available for bash or sh" 276 | fi 277 | 278 | # iterate over each target file 279 | for file in "${names[@]}"; do 280 | # if target exists 281 | if [[ -e $file ]]; then 282 | # if noclobber is on, set file to the new filename (with .N) 283 | if ((nc)); then 284 | file=$(noclobber "$file") 285 | 286 | # otherwise, don't create the file, but try to add it to be edited for -e 287 | else 288 | err "$file already exists" 289 | 290 | if [[ -f $file && -r $file ]]; then 291 | toedit+=("$file") 292 | fi 293 | 294 | continue 295 | fi 296 | fi 297 | 298 | # write the shebang, and possibly other stuff, to the new file 299 | { 300 | printf '%s\n\n' "$shebang" 301 | 302 | case $lang in 303 | bash) 304 | printf '%s\n\n' 'shopt -s extglob nullglob' 305 | ((cron)) && add_path 306 | ;; 307 | sh) 308 | ((cron)) && add_path 309 | ;; 310 | perl) 311 | printf '%s\n%s\n\n' 'use warnings;' 'use strict;' 312 | ;; 313 | esac 314 | } > "$file" 315 | 316 | # make file executable 317 | chmod +x -- "$file" || err "error making $file executable" 318 | # add file to list of files to edit 319 | toedit+=("$file") 320 | done 321 | 322 | # if -e is used, open the file(s) in the preferred editor 323 | if ((edit));then 324 | editor "${toedit[@]}" 325 | fi 326 | 327 | 328 | 329 | # Copyright Daniel Mills 330 | # 331 | # Permission is hereby granted, free of charge, to any person obtaining a copy 332 | # of this software and associated documentation files (the "Software"), to 333 | # deal in the Software without restriction, including without limitation the 334 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 335 | # sell copies of the Software, and to permit persons to whom the Software is 336 | # furnished to do so, subject to the following conditions: 337 | # 338 | # The above copyright notice and this permission notice shall be included in 339 | # all copies or substantial portions of the Software. 340 | # 341 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 342 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 343 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 344 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 345 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 346 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 347 | # IN THE SOFTWARE. 348 | -------------------------------------------------------------------------------- /noclobber: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # noclobber [OPTIONS] FILE 4 | # 5 | # Appends a numeric suffix to FILE, incrementing it until the resulting filename 6 | # no longer exists. Prints the result to the standard output. If FILE already has 7 | # an extension, adds the number before that extension. For example, if FILE is 8 | # `my_file.txt', a possible result would be `my_file.1.txt'. 9 | # 10 | # Options: 11 | # -p, --padding NUM 0-pad the numeric suffix to NUM digits. By default, no 12 | # padding is used. NUM must be a positive integer. 13 | # -e, --extension EXT Use EXT as the extension, adding the number before it. 14 | # By default, the extension used is the shortest match 15 | # from the end starting with `.'. If EXT is not at the 16 | # end of FILE, it will be ignored and the number will be 17 | # added to the end. 18 | # -i, --ignore-ext Ignore any existing extension, and always append the 19 | # number to the end of the filename. This option will 20 | # override --extension. 21 | # -s, --separator SEP Use SEP instead of `.' for the character before the 22 | # number. For example, if SEP is `-', and FILE is 23 | # `my_file.txt', a possible result would be 24 | # `my_file-1.txt'. SEP is required, but may be empty 25 | # This is not used in determining the extension, only for 26 | # adding the number. 27 | # -n, --start NUM Start incrementing the appended number with NUM. Must be 28 | # an integer. The default is 1. 29 | # -c, --ignore-case Perform a case-insensitive test for the existence of 30 | # FILE. By default, the test is case sensitive. This also 31 | # extends to the test for EXT when using --extension. 32 | # -h, --help Display this help and exit 33 | # 34 | # Only one FILE may be provided. If more are supplied, only the first will be 35 | # used. 36 | # 37 | # Note that using this script is not atomic, there is a possibility of a race 38 | # condition. If you want to safely create a temp file, use mktemp(1) or 39 | # similar. See http://mywiki.wooledge.org/BashFAQ/062 for more info. 40 | 41 | 42 | ################################################################################ 43 | 44 | shopt -s extglob 45 | 46 | # initialize variables 47 | padding=0 # number of digits to pad the resulting suffix to 48 | user_ext=0 # bool, whether or not a user-supplied EXT was provided 49 | extension= # user-supplied file extension 50 | has_ext=0 # bool, whether or we care about an extension at all 51 | ignore_ext=0 # bool, whether or not to ignore the extension 52 | ignore_case=0 # bool, whether or not perform case-insensitive tests 53 | sep=. # character to add before the number 54 | base= # filename sans extension 55 | ext= # determined extension 56 | final= # final filename 57 | num=1 # number to append, if any 58 | 59 | # usage: f_exists FILE 60 | # returns 0 if FILE exists, 1 if not. Performs the test based on "$ignore_case" 61 | f_exists() { 62 | # case insensitive test, use a subshell so the shopt doesn't persist 63 | if ((ignore_case)); then 64 | ( 65 | shopt -s nocaseglob 66 | f=( "$1"?() ) 67 | 68 | ((${#f[@]})) 69 | ) 70 | 71 | # case sensitive test, simply use test -e 72 | else 73 | [[ -e $1 ]] 74 | fi 75 | } 76 | 77 | # ...usage 78 | usage() { 79 | cat <<'EOF' 80 | noclobber [OPTIONS] FILE 81 | 82 | Appends a numeric suffix to FILE, incrementing it until the resulting filename 83 | no longer exists. Prints the result to the standard output. If FILE already has 84 | an extension, adds the number before that extension. For example, if FILE is 85 | `my_file.txt', a possible result would be `my_file.1.txt'. 86 | 87 | Options: 88 | -p, --padding NUM 0-pad the numeric suffix to NUM digits. By default, no 89 | padding is used. NUM must be a positive integer. 90 | -e, --extension EXT Use EXT as the extension, adding the number before it. 91 | By default, the extension used is the shortest match 92 | from the end starting with `.'. If EXT is not at the 93 | end of FILE, it will be ignored and the number will be 94 | added to the end. 95 | -i, --ignore-ext Ignore any existing extension, and always append the 96 | number to the end of the filename. This option will 97 | override --extension. 98 | -s, --separator SEP Use SEP instead of `.' for the character before the 99 | number. For example, if SEP is `-', and FILE is 100 | `my_file.txt', a possible result would be 101 | `my_file-1.txt'. SEP is required, but may be empty. 102 | This is not used in determining the extension, only for 103 | adding the number. 104 | -n, --start NUM Start incrementing the appended number with NUM. Must be 105 | an integer. The default is 1. 106 | -c, --ignore-case Perform a case-insensitive test for the existence of 107 | FILE. By default, the test is case sensitive. This also 108 | extends to the test for EXT when using --extension. 109 | -h, --help Display this help and exit 110 | 111 | Only one FILE may be provided. If more are supplied, only the first will be 112 | used. 113 | 114 | Note that using this script is not atomic, there is a possibility of a race 115 | condition. If you want to safely create a temp file, use mktemp(1) or 116 | similar. See http://mywiki.wooledge.org/BashFAQ/062 for more info. 117 | EOF 118 | } 119 | 120 | # option string, for short options. 121 | # very much like getopts, any option followed by a ':' takes a required arg 122 | optstring=p:e:is:n:ch 123 | 124 | unset options 125 | while (($#)); do 126 | case $1 in 127 | -[!-]?*) 128 | for ((i=1; i<${#1}; i++)); do 129 | c=${1:i:1}; options+=("-$c") 130 | if [[ $optstring = *"$c:"* && ${1:i+1} ]]; then 131 | options+=("${1:i+1}") 132 | break 133 | fi 134 | done 135 | ;; 136 | --?*=*) options+=("${1%%=*}" "${1#*=}");; 137 | # end of options, stop breaking them up 138 | --) 139 | options+=(--endopts) 140 | shift 141 | options+=("$@") 142 | break 143 | ;; 144 | *) options+=("$1");; 145 | esac 146 | 147 | shift 148 | done 149 | set -- "${options[@]}" 150 | unset options 151 | 152 | # actually parse the options and do stuff 153 | while [[ $1 = -?* ]]; do 154 | case $1 in 155 | -p|--padding) 156 | # validate NUM 157 | if [[ -z $2 || $2 = *[!0-9]* ]]; then 158 | printf "invalid NUM for \`%s': \`%s'\n" "$1" "$2" >&2 159 | exit 1 160 | fi 161 | 162 | padding=$2 163 | shift 164 | ;; 165 | -e|--extension) 166 | # die if no EXT is provided 167 | if [[ -z $2 ]]; then 168 | printf "no EXT supplied for \`%s'\n" "$1" >&2 169 | exit 1 170 | fi 171 | 172 | user_ext=1 173 | extension=$2 174 | shift 175 | ;; 176 | -i|--ignore-ext) ignore_ext=1;; 177 | -s|--separator) 178 | # die if no SEP is provided 179 | if [[ -z ${2+set} ]]; then 180 | printf "no SEP supplied for \`%s'\n" "$1" >&2 181 | exit 1 182 | fi 183 | 184 | sep=$2 185 | shift 186 | ;; 187 | -n|--start) 188 | # validate NUM 189 | if [[ -z $2 || $2 != ?(-)+([0-9]) ]]; then 190 | printf "invalid NUM for \`%s': \`%s'\n" "$1" "$2" >&2 191 | exit 1 192 | fi 193 | 194 | num=$2 195 | shift 196 | ;; 197 | -c|--ignore-case) ignore_case=1;; 198 | -h|--help) usage >&2; exit 0;; 199 | --endopts) shift; break;; 200 | *) printf 'invalid option: %s\n' "$1" >&2; exit 1;; 201 | esac 202 | 203 | shift 204 | done 205 | 206 | # check to make sure FILE is provided 207 | if [[ -z $1 ]]; then 208 | printf 'No FILE provided\n' >&2 209 | exit 1 210 | fi 211 | 212 | # if FILE doesn't exist, just print the original and exit 213 | if ! f_exists "$1"; then 214 | printf '%s\n' "$1" 215 | exit 0 216 | fi 217 | 218 | # set nocasematch if ignore_case is used 219 | if ((ignore_case)); then 220 | shopt -s nocasematch 221 | fi 222 | 223 | # determine if FILE has an extension we care about and split it accordingly 224 | 225 | # to be ignored, just skip any other checks 226 | if ((ignore_ext)); then 227 | has_ext=0 228 | 229 | # user supplied an extension 230 | elif ((user_ext)); then 231 | if [[ $1 =~ "$extension"$ ]]; then 232 | has_ext=1 233 | base=${1%"${BASH_REMATCH[0]}"} 234 | ext=${BASH_REMATCH[0]} 235 | else 236 | has_ext=0 237 | fi 238 | 239 | # otherwise, check if there's an extension 240 | else 241 | if [[ $1 = *+([^/]).*([^/]) ]]; then 242 | has_ext=1 243 | base=${1%.*} 244 | ext=.${1##*.} 245 | else 246 | has_ext=0 247 | fi 248 | fi 249 | 250 | # actually generate the new filename 251 | final=$1 252 | 253 | # if there's an extension, append the number beforehand 254 | if ((has_ext)); then 255 | while f_exists "$final"; do 256 | printf -v final '%s%s%0*d%s' "$base" "$sep" "$padding" "$((num++))" "$ext" 257 | done 258 | 259 | # otherwise, just stick it on the end 260 | else 261 | while f_exists "$final"; do 262 | printf -v final '%s%s%0*d' "$1" "$sep" "$padding" "$((num++))" 263 | done 264 | fi 265 | 266 | # print the damn thing already 267 | printf '%s\n' "$final" 268 | 269 | 270 | 271 | # Copyright Daniel Mills 272 | # 273 | # Permission is hereby granted, free of charge, to any person obtaining a copy 274 | # of this software and associated documentation files (the "Software"), to 275 | # deal in the Software without restriction, including without limitation the 276 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 277 | # sell copies of the Software, and to permit persons to whom the Software is 278 | # furnished to do so, subject to the following conditions: 279 | # 280 | # The above copyright notice and this permission notice shall be included in 281 | # all copies or substantial portions of the Software. 282 | # 283 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 284 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 285 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 286 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 287 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 288 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 289 | # IN THE SOFTWARE. 290 | -------------------------------------------------------------------------------- /nsplit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/awk -f 2 | 3 | # usage: nsplit -- [OPTIONS] [FILE ...] 4 | # 5 | # The '--' is required, so AWK itself doesn't read the options 6 | # 7 | # Split FILE (stdin if not provided) into fixed-sized pieces of PREFIXn; default 8 | # size is 1000 lines, and default PREFIX is "out". "n" is a number, starting with 9 | # 0. 10 | # 11 | # Options: 12 | # -h, --help Display this help and exit 13 | # -s, --start NUM Start counting at NUM instead of 0. NUM must be an integer 14 | # -p, --prefix PRE Use PRE as the output prefix 15 | # -l, --lines NUM Split file into pieces of NUM lines. NUM must be a positive 16 | # integer 17 | # -0, --pad NUM Pad suffix to NUM digits with zeroes. NUM must be a 18 | # positive integer 19 | 20 | 21 | 22 | ## usage: getopts(optstring [, longopt_array ]) 23 | ## Parses options, and deletes them from ARGV. "optstring" is of the form 24 | ## "ab:c". Each letter is a possible option. If the letter is followed by a 25 | ## colon (:), then the option requires an argument. If an argument is not 26 | ## provided, or an invalid option is given, getopts will print the appropriate 27 | ## error message and return "?". Returns each option as it's read, and -1 when 28 | ## no options are left. "optind" will be set to the index of the next 29 | ## non-option argument when finished. "optarg" will be set to the option's 30 | ## argument, when provided. If not provided, "optarg" will be empty. "optname" 31 | ## will be set to the current option, as provided. Getopts will delete each 32 | ## option and argument that it successfully reads, so awk will be able to treat 33 | ## whatever's left as filenames/assignments, as usual. If provided, 34 | ## "longopt_array" is the name of an associative array that maps long options to 35 | ## the appropriate short option (do not include the hyphens on either). 36 | ## Sample usage can be found in the examples dir, with gawk extensions, or in 37 | ## the ogrep script for a POSIX example: https://github.com/e36freak/ogrep 38 | function getopts(optstring, longarr, opt, trimmed, hasarg, repeat) { 39 | hasarg = repeat = 0; 40 | optarg = ""; 41 | # increment optind 42 | optind++; 43 | 44 | # return -1 if the current arg is not an option or there are no args left 45 | if (ARGV[optind] !~ /^-/ || optind >= ARGC) { 46 | return -1; 47 | } 48 | 49 | # if option is "--" (end of options), delete arg and return -1 50 | if (ARGV[optind] == "--") { 51 | for (i=1; i<=optind; i++) { 52 | delete ARGV[i]; 53 | } 54 | return -1; 55 | } 56 | 57 | # if the option is a long argument... 58 | if (ARGV[optind] ~ /^--/) { 59 | # trim hyphens 60 | trimmed = substr(ARGV[optind], 3); 61 | # if of the format --foo=bar, split the two. assign "bar" to optarg and 62 | # set hasarg to 1 63 | if (trimmed ~ /=/) { 64 | optarg = trimmed; 65 | sub(/=.*/, "", trimmed); sub(/^[^=]*=/, "", optarg); 66 | hasarg = 1; 67 | } 68 | 69 | # invalid long opt 70 | if (!(trimmed in longarr)) { 71 | printf("unrecognized option -- '%s'\n", ARGV[optind]) > "/dev/stderr"; 72 | return "?"; 73 | } 74 | 75 | opt = longarr[trimmed]; 76 | # set optname by prepending dashes to the trimmed argument 77 | optname = "--" trimmed; 78 | 79 | # otherwise, it is a short option 80 | } else { 81 | # remove the hyphen, and get just the option letter 82 | opt = substr(ARGV[optind], 2, 1); 83 | # set trimmed to whatevers left 84 | trimmed = substr(ARGV[optind], 3); 85 | 86 | # invalid option 87 | if (!index(optstring, opt)) { 88 | printf("invalid option -- '%s'\n", opt) > "/dev/stderr"; 89 | return "?"; 90 | } 91 | 92 | # if there is more to the argument than just -o 93 | if (length(trimmed)) { 94 | # if option requires an argument, set the rest to optarg and hasarg to 1 95 | if (index(optstring, opt ":")) { 96 | optarg = trimmed; 97 | hasarg = 1; 98 | 99 | # otherwise, prepend a hyphen to the rest and set repeat to 1, so the 100 | # same arg is processed again without the first option 101 | } else { 102 | ARGV[optind] = "-" trimmed; 103 | repeat = 1; 104 | } 105 | } 106 | 107 | # set optname by prepending a hypen to opt 108 | optname = "-" opt; 109 | } 110 | 111 | # if the option requires an arg and hasarg is 0 112 | if (index(optstring, opt ":") && !hasarg) { 113 | # increment optind, check if no arguments are left 114 | if (++optind >= ARGC) { 115 | printf("option requires an argument -- '%s'\n", optname) > "/dev/stderr"; 116 | return "?"; 117 | } 118 | 119 | # set optarg 120 | optarg = ARGV[optind]; 121 | 122 | # if repeat is set, decrement optind so we process the same arg again 123 | # mutually exclusive to needing an argument, otherwise hasarg would be set 124 | } else if (repeat) { 125 | optind--; 126 | } 127 | 128 | # delete all arguments up to this point, just to make sure 129 | for (i=1; i<=optind; i++) { 130 | delete ARGV[i]; 131 | } 132 | 133 | # return the option letter 134 | return opt; 135 | } 136 | 137 | ## usage: isnum(string) 138 | ## returns 1 if "string" is a valid number, otherwise 0 139 | function isnum(str) { 140 | # use a regex comparison because 'str == str + 0' has issues with some floats 141 | if (str !~ /^-?[0-9.]+$/ || str ~ /\..*\./) { 142 | return 0; 143 | } 144 | 145 | return 1; 146 | } 147 | 148 | # prints usage information 149 | function usage() { 150 | printf("%s\n\n%s\n\n%s\n%s\n%s\n\n", 151 | "usage: nsplit -- [OPTIONS] [FILE ...]", 152 | "The '--' is required, so AWK itself doesn't read the options", 153 | "Split FILE (stdin if not provided) into fixed-sized pieces of PREFIXn; default", 154 | "size is 1000 lines, and default PREFIX is \"out\". \"n\" is a number, starting with", 155 | "0.") > "/dev/stderr"; 156 | printf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n", 157 | " Options:", 158 | " -h, --help Display this help and exit", 159 | " -s, --start NUM Start counting at NUM instead of 0. NUM must be an integer", 160 | " -p, --prefix PRE Use PRE as the output prefix", 161 | " -l, --lines NUM Split file into pieces of NUM lines. NUM must be a positive", 162 | " integer.", 163 | " -0, --pad NUM Pad suffix to NUM digits with zeroes. NUM must be a", 164 | " positive integer.") > "/dev/stderr"; 165 | } 166 | 167 | 168 | 169 | BEGIN { 170 | # initialize default variables 171 | err = suffix = padding = 0; 172 | prefix = "out"; 173 | lines = 1000; 174 | outfile = ""; 175 | 176 | # map long options to the appropriate short ones 177 | longopts["help"] = "h"; 178 | longopts["start"] = "s"; 179 | longopts["prefix"] = "p"; 180 | longopts["lines"] = "l"; 181 | longopts["pad"] = "0"; 182 | 183 | # parse the options 184 | while ((opt = getopts("hs:p:l:0:")) != -1) { 185 | # -h, --help 186 | if (opt == "h") { 187 | usage(); exit; 188 | 189 | # -s, --start NUM 190 | } else if (opt == "s") { 191 | if (!isnum(optarg)) { 192 | printf("%s is not a valid integer\n", optarg) > "/dev/stderr"; 193 | err = 1; exit; 194 | } 195 | suffix = optarg; 196 | 197 | # -p, --prefix PRE 198 | } else if (opt == "p") { 199 | prefix = optarg; 200 | 201 | # -l, --lines NUM 202 | } else if (opt == "l") { 203 | if (!isnum(optarg)) { 204 | printf("%s is not a valid positive integer\n", optarg) > "/dev/stderr"; 205 | err = 1; exit; 206 | } 207 | lines = optarg; 208 | 209 | # -0, --pad NUM 210 | } else if (opt == "0") { 211 | if (!isnum(optarg)) { 212 | printf("%s is not a valid positive integer\n", optarg) > "/dev/stderr"; 213 | err = 1; exit; 214 | } 215 | padding = optarg; 216 | 217 | # error 218 | } else { 219 | err = 1; exit; 220 | } 221 | } 222 | 223 | # create initial filename 224 | outfile = sprintf("%s%0*d", prefix, padding, suffix); 225 | } 226 | 227 | # write line to current output file 228 | { 229 | print > outfile; 230 | } 231 | 232 | # at every "lines"th line, close the previous output file and increment 233 | !(NR % lines) { 234 | close(outfile); 235 | 236 | outfile = sprintf("%s%0*d", prefix, padding, ++suffix); 237 | } 238 | 239 | # close the last output file after all input is read, and exit 240 | END { 241 | close(outfile); 242 | 243 | exit err; 244 | } 245 | 246 | # Copyright Daniel Mills 247 | # 248 | # Permission is hereby granted, free of charge, to any person obtaining a copy 249 | # of this software and associated documentation files (the "Software"), to 250 | # deal in the Software without restriction, including without limitation the 251 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 252 | # sell copies of the Software, and to permit persons to whom the Software is 253 | # furnished to do so, subject to the following conditions: 254 | # 255 | # The above copyright notice and this permission notice shall be included in 256 | # all copies or substantial portions of the Software. 257 | # 258 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 259 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 260 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 261 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 262 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 263 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 264 | # IN THE SOFTWARE. 265 | -------------------------------------------------------------------------------- /psort: -------------------------------------------------------------------------------- 1 | #!/usr/bin/awk -f 2 | 3 | # This script is completely POSIX compliant. 4 | 5 | # usage: psort -- [OPTIONS] [FILE ...] 6 | # 7 | # The '--' is required, so AWK itself doesn't read the options 8 | # 9 | # Sort FILE, or the standard input if FILE is not provided or if a single 10 | # hyphen (-) is given as a filename, according to the following rules: 11 | # 12 | # - In the following rules, the term REGEX refers to POSIX Extended Regular 13 | # Expressions valid for your version of awk, provided to this program with 14 | # the -p option. 15 | # - When sorting, values matching a REGEX will take priority over any other 16 | # values. 17 | # - Each REGEX will have priority in ascending order, according to the order 18 | # in which they were given as arguments. The first REGEX will have priority 19 | # over the second and third, etc. 20 | # - Values both matching the same REGEX will be sorted against each other as 21 | # strings, as usual. 22 | # - All other values will be sorted as strings, in ascending order. 23 | # - Uses the quicksort algorithm, combined with mergesort for large inputs. 24 | # - Prints the result to the standard output. 25 | # 26 | # Options: 27 | # -p, --patt REGEX Sort values matching REGEX higher than other values, 28 | # according to the rules above. Can be supplied multiple 29 | # times. The first supplied will have higher priority than 30 | # the second, etc. 31 | # -f, --file FILE Obtain REGEXs from FILE, one per line. 32 | # -g, --general Sort according to string comparisons (the default). 33 | # -n, --numeric Sort according to string numeric value. 34 | # -r, --reverse Reverse the result of comparisons 35 | # -t, --sep SEP Use SEP for the field separator, instead of non-blank to 36 | # blank transitions. SEP follows the same rules as FS for 37 | # your version of awk. 38 | # -k, --key KEYDEF Sort via a key; KEYDEF gives location and type. See the 39 | # description below. 40 | # -i, --ignore-case Do a case-insensitive sort. Also makes pattern matching 41 | # against REGEXs case-insensitive. 42 | # -s, --stable Stabilize sort by disabling last-resort comparison. 43 | # -h, --help Display this help and exit. 44 | # 45 | # KEYDEF Definition: 46 | # KEYDEF is F[,F][OPTS], for start and stop position, where F is a field number 47 | # with origin 1. The stop position defaults to the line's end. OPTS is one or 48 | # more single-letter ordering options [gnrix], which override global ordering 49 | # options for that key. "g", "n", "r", and "i" do the same thing as their 50 | # respective options above, while "x" ignores REGEXs for that key. 51 | # 52 | # Example Usage: 53 | # psort -- -p '^foo' -p '^bar' myfile.txt 54 | # This will sort 'myfile.txt', with all values matching '^foo' first, 55 | # followed by all values matching '^bar', followed by everything else. 56 | 57 | # TODO: 58 | # add -z (not sure if this can be done POSIXly) 59 | 60 | 61 | 62 | # base comparison function 63 | # compares "a" and "b", returning 0 for false and 1 for true, according to the 64 | # options in the "opts" array see the __pcompare() and psort() descriptions for 65 | # more detail 66 | function __bcompare(a, b, opts) { 67 | # force numeric comparison 68 | if (opts["n"]) { 69 | return a + 0 < b + 0; 70 | 71 | # force string comparison 72 | } else { 73 | return "a" a < "a" b; 74 | } 75 | } 76 | 77 | # comparison function 78 | # compares "a" and "b" based on "patts", returning 0 for false and 1 for true 79 | # compares according to the options in the "opts" array. see the psort() 80 | # description for more detail 81 | function __pcompare(a, b, patts, plen, opts, p) { 82 | # "x" was used, just do a basic comparison 83 | if (opts["x"]) { 84 | return opts["r"] ? !__bcompare(a, b, opts) : __bcompare(a, b, opts); 85 | } 86 | 87 | # loop over each regex in order, and check if either value matches 88 | for (p=1; p<=plen; p++) { 89 | # if the first matches... 90 | if (a ~ patts[p]) { 91 | # check if the second also matches. if so, do a normal comparison 92 | if (b ~ patts[p]) { 93 | return opts["r"] ? !__bcompare(a, b, opts) : __bcompare(a, b, opts); 94 | 95 | # second doesn't match, the first sorts higher 96 | } else { 97 | return !opts["r"]; 98 | } 99 | 100 | # if the second matches here, the first didn't, so the second sorts higher 101 | } else if (b ~ patts[p]) { 102 | return opts["r"]; 103 | } 104 | } 105 | 106 | # no regex matched, do a normal comparison 107 | return opts["r"] ? !__bcompare(a, b, opts) : __bcompare(a, b, opts); 108 | } 109 | 110 | # actual sorting function 111 | # sorts the values in "array" in-place, from indices "left" to "right", based 112 | # on an array of regular expressions (see the psort() description) 113 | function __pquicksort(array, left, right, patts, plen, opts, piv, mid, tmp) { 114 | # return if array contains one element or less 115 | if ((right - left) <= 0) { 116 | return; 117 | } 118 | 119 | # choose random pivot 120 | piv = int(rand() * (right - left + 1)) + left; 121 | 122 | # swap left and pivot 123 | tmp = array[piv]; 124 | array[piv] = array[left]; 125 | array[left] = tmp; 126 | 127 | mid = left; 128 | # iterate over each element from the second to the last, and compare 129 | for (piv=left+1; piv<=right; piv++) { 130 | # if the comparison based on "how" is true... 131 | if (__pcompare(array[piv], array[left], patts, plen, opts)) { 132 | # increment mid 133 | mid++; 134 | 135 | # swap mid and pivot 136 | tmp = array[piv]; 137 | array[piv] = array[mid]; 138 | array[mid] = tmp; 139 | } 140 | } 141 | 142 | # swap left and mid 143 | tmp = array[mid]; 144 | array[mid] = array[left]; 145 | array[left] = tmp; 146 | 147 | # recursively sort the two halves 148 | __pquicksort(array, left, mid - 1, patts, plen, opts); 149 | __pquicksort(array, mid + 1, right, patts, plen, opts); 150 | } 151 | 152 | ## usage: extract_range(string, start, stop) 153 | ## extracts fields "start" through "stop" from "string", based on FS, with the 154 | ## original field separators intact. returns the extracted fields. 155 | function extract_range(str, start, stop, i, re, out) { 156 | # if FS is the default, trim leading and trailing spaces from "string" and 157 | # set "re" to the appropriate regex 158 | if (FS == " ") { 159 | gsub(/^[[:space:]]+|[[:space:]]+$/, "", str); 160 | re = "[[:space:]]+"; 161 | } else { 162 | re = FS; 163 | } 164 | 165 | # remove fields 1 through start - 1 from the beginning 166 | for (i=1; i= ARGC) { 223 | return -1; 224 | } 225 | 226 | # if option is "--" (end of options), delete arg and return -1 227 | if (ARGV[optind] == "--") { 228 | for (i=1; i<=optind; i++) { 229 | delete ARGV[i]; 230 | } 231 | return -1; 232 | } 233 | 234 | # if the option is a long argument... 235 | if (ARGV[optind] ~ /^--/) { 236 | # trim hyphens 237 | trimmed = substr(ARGV[optind], 3); 238 | # if of the format --foo=bar, split the two. assign "bar" to optarg and 239 | # set hasarg to 1 240 | if (trimmed ~ /=/) { 241 | optarg = trimmed; 242 | sub(/=.*/, "", trimmed); sub(/^[^=]*=/, "", optarg); 243 | hasarg = 1; 244 | } 245 | 246 | # invalid long opt 247 | if (!(trimmed in longarr)) { 248 | printf("unrecognized option -- '%s'\n", ARGV[optind]) > "/dev/stderr"; 249 | return "?"; 250 | } 251 | 252 | opt = longarr[trimmed]; 253 | # set optname by prepending dashes to the trimmed argument 254 | optname = "--" trimmed; 255 | 256 | # otherwise, it is a short option 257 | } else { 258 | # remove the hyphen, and get just the option letter 259 | opt = substr(ARGV[optind], 2, 1); 260 | # set trimmed to whatevers left 261 | trimmed = substr(ARGV[optind], 3); 262 | 263 | # invalid option 264 | if (!index(optstring, opt)) { 265 | printf("invalid option -- '%s'\n", opt) > "/dev/stderr"; 266 | return "?"; 267 | } 268 | 269 | # if there is more to the argument than just -o 270 | if (length(trimmed)) { 271 | # if option requires an argument, set the rest to optarg and hasarg to 1 272 | if (index(optstring, opt ":")) { 273 | optarg = trimmed; 274 | hasarg = 1; 275 | 276 | # otherwise, prepend a hyphen to the rest and set repeat to 1, so the 277 | # same arg is processed again without the first option 278 | } else { 279 | ARGV[optind] = "-" trimmed; 280 | repeat = 1; 281 | } 282 | } 283 | 284 | # set optname by prepending a hypen to opt 285 | optname = "-" opt; 286 | } 287 | 288 | # if the option requires an arg and hasarg is 0 289 | if (index(optstring, opt ":") && !hasarg) { 290 | # increment optind, check if no arguments are left 291 | if (++optind >= ARGC) { 292 | printf("option requires an argument -- '%s'\n", optname) > "/dev/stderr"; 293 | return "?"; 294 | } 295 | 296 | # set optarg 297 | optarg = ARGV[optind]; 298 | 299 | # if repeat is set, decrement optind so we process the same arg again 300 | # mutually exclusive to needing an argument, otherwise hasarg would be set 301 | } else if (repeat) { 302 | optind--; 303 | } 304 | 305 | # delete all arguments up to this point, just to make sure 306 | for (i=1; i<=optind; i++) { 307 | delete ARGV[i]; 308 | } 309 | 310 | # return the option letter 311 | return opt; 312 | } 313 | 314 | # usage: mainsort(array, alen, patts, plen, keynum) 315 | # extracts the keys and sorts "array" in place, according to the rules in the 316 | # psorti() function and command usage. "alen" is the length of the array to be 317 | # sorted. "patts" is an array of patterns, numerically indexed with origin 1. 318 | # "plen" is the length of the "patts" array. "keynum" is the number of the first 319 | # array index of the global "k*" arrays to use, or 0 if no key was provided. 320 | # returns the length, or 0 if no sorting is to be done (stable was used and 321 | # no more keys are left) 322 | function mainsort(arr, alen, patts, plen, keynum, 323 | key, tosort, lines, counts, opts, tpatts, tarr, tlen, i, j, o) { 324 | # make sure "keynum" exists, if not set to 0 325 | if (keynum && !kstarts[keynum]) { 326 | if (stable) { 327 | return 0; 328 | } else { 329 | keynum = 0; 330 | } 331 | } 332 | 333 | # generate temporary options array for the current key 334 | # if we're in a key and said key has options, use them 335 | if (keynum && length(kopts[keynum])) { 336 | # defaults 337 | opts["g"] = 1; # generic sort 338 | opts["n"] = 0; # numeric sort 339 | opts["r"] = 0; # reverse 340 | opts["i"] = 0; # ignore-case 341 | opts["x"] = 0; # ignore patts 342 | 343 | # loop over each letter in the options 344 | tlen = split(kopts[keynum], tarr, ""); 345 | for (i=1; i<=tlen; i++) { 346 | # handle mutually exclusive options 347 | if (tarr[i] == "n") { 348 | opts["g"] = 0; 349 | } else if (tarr[i] == "g") { 350 | opts["n"] = 0; 351 | } 352 | 353 | opts[tarr[i]] = 1; 354 | } 355 | 356 | # not in a key or key has no options, use global 357 | } else { 358 | for (i in options) { 359 | opts[i] = options[i]; 360 | } 361 | } 362 | 363 | # generate temporary pattern array for the current key 364 | if (!opts["x"]) { 365 | if (opts["i"]) { 366 | for (i=1; i<=plen; i++) { 367 | tpatts[i] = tolower(patts[i]); 368 | } 369 | } else { 370 | for (i=1; i<=plen; i++) { 371 | tpatts[i] = patts[i]; 372 | } 373 | } 374 | } 375 | 376 | # generate arrays for sorting on the current key 377 | for (i=1; i<=alen; i++) { 378 | if (keynum) { 379 | if (opts["i"]) { 380 | key = tolower(extract_range(arr[i], kstarts[keynum], 381 | kstops[keynum] ? kstops[keynum] : NF)); 382 | } else { 383 | key = extract_range(arr[i], kstarts[keynum], 384 | kstops[keynum] ? kstops[keynum] : NF); 385 | } 386 | 387 | # use the whole line 388 | } else { 389 | if (opts["i"]) { 390 | key = tolower(arr[i]); 391 | } else { 392 | key = arr[i]; 393 | } 394 | } 395 | 396 | tosort[key]; 397 | lines[key,++counts[key]] = arr[i]; 398 | } 399 | 400 | # sort keys 401 | alen = psorti(tosort, tpatts, plen, opts); 402 | 403 | o = 0; 404 | # overwrite original array with the sorted one, calling recursively where needed 405 | for (i=1; i<=alen; i++) { 406 | tlen = counts[tosort[i]]; 407 | 408 | # if there's only one line for the current key, append it to the main array 409 | if (tlen == 1) { 410 | arr[++o] = lines[tosort[i],tlen]; 411 | 412 | # if there's more than one line for the key... 413 | } else { 414 | # create a temp array of those lines 415 | for (j=1; j<=tlen; j++) { 416 | tarr[j] = lines[tosort[i],j]; 417 | } 418 | 419 | # sort the temp array on the next key 420 | mainsort(tarr, tlen, patts, plen, keynum + 1); 421 | 422 | # append the results to the main array 423 | for (j=1; j<=tlen; j++) { 424 | arr[++o] = lines[tosort[i],j]; 425 | } 426 | } 427 | } 428 | 429 | return o; 430 | } 431 | 432 | ## usage: mktemp(template [, type]) 433 | ## creates a temporary file or directory, safely, and returns its name. 434 | ## if template is not a pathname, the file will be created in ENVIRON["TMPDIR"] 435 | ## if set, otherwise /tmp. the last six characters of template must be "XXXXXX", 436 | ## and these are replaced with a string that makes the filename unique. type, if 437 | ## supplied, is either "f", "d", or "u": for file, directory, or dry run (just 438 | ## returns the name, doesn't create a file), respectively. If template is not 439 | ## provided, uses "tmp.XXXXXX". Files are created u+rw, and directories u+rwx, 440 | ## minus umask restrictions. returns -1 if an error occurs. 441 | function mktemp(template, type, 442 | c, chars, len, dir, dir_esc, rstring, i, out, out_esc, umask, 443 | cmd) { 444 | # portable filename characters 445 | c = "012345689ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 446 | len = split(c, chars, ""); 447 | 448 | # make sure template is valid 449 | if (length(template)) { 450 | if (template !~ /XXXXXX$/) { 451 | return -1; 452 | } 453 | 454 | # template was not supplied, use the default 455 | } else { 456 | template = "tmp.XXXXXX"; 457 | } 458 | 459 | # make sure type is valid 460 | if (length(type)) { 461 | if (type !~ /^[fdu]$/) { 462 | return -1; 463 | } 464 | 465 | # type was not supplied, use the default 466 | } else { 467 | type = "f"; 468 | } 469 | 470 | # if template is a path... 471 | if (template ~ /\//) { 472 | dir = template; 473 | sub(/\/[^/]*$/, "", dir); 474 | sub(/.*\//, "", template); 475 | 476 | # template is not a path, determine base dir 477 | } else { 478 | if (length(ENVIRON["TMPDIR"])) { 479 | dir = ENVIRON["TMPDIR"]; 480 | } else { 481 | dir = "/tmp"; 482 | } 483 | } 484 | 485 | # escape dir for shell commands 486 | esc_dir = dir; 487 | sub(/'/, "'\\''", esc_dir); 488 | esc_dir = "'" esc_dir "'"; 489 | 490 | # if this is not a dry run, make sure the dir exists 491 | if (type != "u" && system("test -d " esc_dir)) { 492 | return -1; 493 | } 494 | 495 | # get the base of the template, sans Xs 496 | template = substr(template, 0, length(template) - 6); 497 | 498 | # generate the filename 499 | do { 500 | rstring = ""; 501 | for (i=0; i<6; i++) { 502 | c = chars[int(rand() * len) + 1]; 503 | rstring = rstring c; 504 | } 505 | 506 | out_esc = out = dir "/" template rstring; 507 | sub(/'/, "'\\''", out_esc); 508 | out_esc = "'" out_esc "'"; 509 | } while (!system("test -e " out_esc)); 510 | 511 | # if needed, create the filename 512 | if (type == "f") { 513 | system("touch " out_esc); 514 | cmd = "umask"; 515 | cmd | getline umask; 516 | close(cmd); 517 | umask = substr(umask, 2, 1); 518 | system("chmod 0" 6 - umask "00 " out_esc); 519 | } else if (type == "d") { 520 | system("mkdir " out_esc); 521 | cmd = "umask"; 522 | cmd | getline umask; 523 | close(cmd); 524 | umask = substr(umask, 2, 1); 525 | system("chmod 0" 7 - umask "00 " out_esc); 526 | } 527 | 528 | # return the filename 529 | return out; 530 | } 531 | 532 | # usage: parsekey(key, a) 533 | # Validates "key", and parses the start, stop, and opts portions from it and 534 | # assigns them to the array "a". the start portion will be in a[1], stop in 535 | # a[2], and opts in a[3]. returns 1 if "key" is valid, otherwise 0. 536 | function parsekey(key, arr) { 537 | # validate inital format 538 | if (key !~ /^[1-9][0-9]*(,[1-9][0-9]*)?[gnrix]*$/) { 539 | return 0; 540 | } 541 | 542 | # empty array 543 | split("", arr); 544 | 545 | # no stop portion 546 | if (!index(key, ",")) { 547 | arr[1] = arr[3] = key; 548 | sub(/[[:alpha:]]*$/, "", arr[1]); 549 | sub(/^[0-9]*/, "", arr[3]); 550 | arr[1] = +arr[1] 551 | 552 | return 1; 553 | } 554 | 555 | # start and stop portions 556 | arr[1] = arr[2] = arr[3] = key; 557 | sub(/,.*/, "", arr[1]); 558 | gsub(/.*,|[[:alpha:]]*$/, "", arr[2]); 559 | sub(/[^[:alpha:]]*/, "", arr[3]); 560 | arr[1] = +arr[1]; 561 | arr[2] = +arr[2]; 562 | 563 | # make sure stop is >= start 564 | if (arr[2] < arr[1]) { 565 | return 0; 566 | } 567 | 568 | return 1; 569 | } 570 | 571 | # usage: psorti(s, p, len, opts) 572 | # Sorts the indices of the array "s" in place, based on the rules below. When 573 | # finished, the array is indexed numerically starting with 1, and the values 574 | # are those of the original indices. "p" is a compact (non-sparse) 1-indexed 575 | # array containing regular expressions. "len" is the length of the "p" array. 576 | # Returns the length of the "s" array. "opts" is an associative array of the 577 | # options used for sorting, the index is the option and the value is 1 or 0 578 | # (true or false, respectively). Uses the quicksort algorithm, with a random 579 | # pivot to help avoid worst-case behavior on already sorted arrays. Requires 580 | # the __pcompare() and __pquicksort() functions. 581 | # 582 | # Sorting rules: 583 | # - When sorting, values matching an expression in the "p" array will take 584 | # priority over any other values. 585 | # - Each expression in the "p" array will have priority in ascending order 586 | # by index. "p[1]" will have priority over "p[2]" and "p[3]", etc. 587 | # - Values both matching the same regex will be compared to each other as 588 | # strings, as usual. 589 | # - All other values will be compared as strings. 590 | # 591 | # For example: 592 | # patts[1]="^foo"; patts[2]="^bar"; len = psort(arr, patts, 2, opt_array, k); 593 | # This will sort the indices of "arr" in place, with all values matching 594 | # '^foo' first, followed by all values matching '^bar', followed by everything 595 | # else. in this example, "opt_array" is empty and "k" is 0. 596 | function psorti(array, patts, plen, opts, tmp, count, i) { 597 | # loop over each index, and generate a new array with the original indices 598 | # mapped to new numeric ones 599 | count = 0; 600 | for (i in array) { 601 | tmp[++count] = i; 602 | delete array[i]; 603 | } 604 | 605 | # copy tmp back over the original array 606 | for (i=1; i<=count; i++) { 607 | array[i] = tmp[i]; 608 | delete tmp[i]; 609 | } 610 | 611 | # seed the random number generator 612 | srand(); 613 | 614 | # actually sort 615 | __pquicksort(array, 1, count, patts, plen, opts); 616 | 617 | # return the length 618 | return count; 619 | } 620 | 621 | ## usage: shell_esc(string) 622 | ## returns the string escaped so that it can be used in a shell command 623 | function shell_esc(str) { 624 | gsub(/'/, "'\\''", str); 625 | 626 | return "'" str "'"; 627 | } 628 | 629 | # Display usage information 630 | function usage() { 631 | printf("%s\n\n%s\n\n%s\n%s\n\n", 632 | "usage: psort -- [OPTIONS] [FILE ...]", 633 | "The '--' is required, so AWK itself doesn't read the options", 634 | "Sort FILE, or the standard input if FILE is not provided or if a single", 635 | "hyphen (-) is given as a filename, according to the following rules:" \ 636 | ) > "/dev/stderr"; 637 | printf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n\n", 638 | " - In the following rules, the term REGEX refers to POSIX Extended Regular", 639 | " Expressions valid for your version of awk, provided to this program with", 640 | " the -p option.", 641 | " - When sorting, values matching a REGEX will take priority over any other", 642 | " values.", 643 | " - Each REGEX will have priority in ascending order, according to the order", 644 | " in which they were given as arguments. The first REGEX will have priority", 645 | " over the second and third, etc.", 646 | " - Values both matching the same REGEX will be sorted against each other as", 647 | " strings, as usual.", 648 | " - All other values will be sorted as strings, in ascending order.", 649 | " - Uses the quicksort algorithm, combined with mergesort for large inputs.", 650 | " - Prints the result to the standard output." \ 651 | ) > "/dev/stderr"; 652 | printf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n", 653 | " Options:", 654 | " -p, --patt REGEX Sort values matching REGEX higher than other values,", 655 | " according to the rules above. Can be supplied multiple", 656 | " times. The first supplied will have higher priority than", 657 | " the second, etc.", 658 | " -f, --file FILE Obtain REGEXs from FILE, one per line.", 659 | " -g, --general Sort according to string comparisons (the default).", 660 | " -n, --numeric Sort according to string numeric value.", 661 | " -r, --reverse Reverse the result of comparisons" \ 662 | ) > "/dev/stderr"; 663 | printf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n\n", 664 | " -t, --sep SEP Use SEP for the field separator, instead of non-blank to", 665 | " blank transitions. SEP follows the same rules as FS for", 666 | " your version of awk.", 667 | " -k, --key KEYDEF Sort via a key; KEYDEF gives location and type. See the", 668 | " description below.", 669 | " -i, --ignore-case Do a case-insensitive sort. Also makes pattern matching", 670 | " against REGEXs case-insensitive.", 671 | " -s, --stable Stabilize sort by disabling last-resort comparison.", 672 | " -h, --help Display this help and exit." \ 673 | ) > "/dev/stderr"; 674 | printf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s\n%s\n%s\n%s\n", 675 | "KEYDEF Definition:", 676 | " KEYDEF is F[,F][OPTS], for start and stop position, where F is a field number", 677 | " with origin 1. The stop position defaults to the line's end. OPTS is one or", 678 | " more single-letter ordering options [gnrix], which override global ordering", 679 | " options for that key. "g", "n", "r", and "i" do the same thing as their", 680 | " respective options above, while "x" ignores REGEXs for that key.", 681 | "Example Usage:", 682 | " psort -- -p '^foo' -p '^bar' myfile.txt", 683 | " This will sort 'myfile.txt', with all values matching '^foo' first,", 684 | " followed by all values matching '^bar', followed by everything else." \ 685 | ) > "/dev/stderr"; 686 | } 687 | 688 | 689 | 690 | BEGIN { 691 | # initialize default variables 692 | toexit = err = 0; 693 | p = k = stable = count = tmpcount = 0; 694 | buff_max = 50000; # maximum number of lines to buffer 695 | 696 | # default global sorting options 697 | options["g"] = 1; # generic sort 698 | options["n"] = 0; # numeric sort 699 | options["r"] = 0; # reverse 700 | options["i"] = 0; # ignore-case 701 | options["x"] = 0; # ignore patts (not actually available globally) 702 | 703 | # map long options to the appropriate short ones 704 | longopts["patt"] = "p"; 705 | longopts["file"] = "f"; 706 | longopts["general"] = "g"; 707 | longopts["numeric"] = "n"; 708 | longopts["reverse"] = "r"; 709 | longopts["sep"] = "t"; 710 | longopts["key"] = "k"; 711 | longopts["ignore-case"] = "i"; 712 | longopts["stable"] = "s"; 713 | longopts["help"] = "h"; 714 | 715 | # read and parse the options. can't use switch(), remaining POSIX compliant 716 | while ((opt = getopts("p:f:gnrt:k:ish", longopts)) != -1) { 717 | # -p, --patt REGEX 718 | if (opt == "p") { 719 | patts[++p] = optarg; 720 | 721 | # -f, --file FILE 722 | } else if (opt == "f") { 723 | # check to make sure FILE exists, is readable, or is stdin 724 | if (optarg == "-" || optarg == "/dev/stdin") { 725 | file = "/dev/stdin"; 726 | } else { 727 | f = shell_esc(optarg); 728 | 729 | if ((system("test -f " f) && system("test -p " f)) || 730 | system("test -r " f)) { 731 | printf("%s: Permission denied\n", optarg) > "/dev/stderr"; 732 | err = toexit = 1; exit; 733 | } 734 | 735 | file = optarg; 736 | } 737 | 738 | # read each line from FILE, add to patterns 739 | while ((getline patts[++p] < file) > 0); 740 | # decrement "p", getline still set it during the last call 741 | p--; 742 | 743 | # -g, --general 744 | } else if (opt == "g") { 745 | options["g"] = 1; 746 | options["n"] = 0; 747 | 748 | # -n, --numeric 749 | } else if (opt == "n") { 750 | options["n"] = 1; 751 | options["g"] = 0; 752 | 753 | # -r, --reverse 754 | } else if (opt == "r") { 755 | options["r"] = 1; 756 | 757 | # -t, --sep SEP 758 | } else if (opt == "t") { 759 | FS = optarg; 760 | 761 | # -k, --key KEY 762 | } else if (opt == "k") { 763 | # check to make sure KEYDEF is valid, and extract parts 764 | if (!parsekey(optarg, tmp)) { 765 | printf("%s: invalid KEYDEF\n", optarg) > "/dev/stderr"; 766 | err = toexit = 1; exit; 767 | } 768 | 769 | # assign parts to arrays 770 | k++; 771 | kstarts[k] = tmp[1]; 772 | kstops[k] = tmp[2]; 773 | kopts[k] = tmp[3]; 774 | 775 | # -i, --ignore-case 776 | } else if (opt == "i") { 777 | options["i"] = 1; 778 | 779 | # -s, --stable 780 | } else if (opt == "s") { 781 | stable = 1; 782 | 783 | # -h, --help 784 | } else if (opt == "h") { 785 | usage(); 786 | toexit = 1; exit; 787 | 788 | # error 789 | } else { 790 | err = toexit = 1; exit; 791 | } 792 | } 793 | } 794 | 795 | # read each line into an array 796 | { 797 | lines[++count] = $0; 798 | } 799 | 800 | # when we hit the number of maximum lines to buffer 801 | !(NR % buff_max) { 802 | # sort what's currently in the buffer 803 | outlen = mainsort(lines, count, patts, p, !!k); 804 | 805 | # create a temp file 806 | temps[++tmpcount] = mktemp("psort.XXXXXX"); 807 | 808 | # write the sorted lines to the temp file 809 | for (i=1; i<=outlen; i++) { 810 | print lines[i] > temps[tmpcount]; 811 | } 812 | close(temps[tmpcount]); 813 | 814 | # reset the line count 815 | count = 0; 816 | } 817 | 818 | END { 819 | # do not process any further if toexit is set (exit was called) 820 | if (toexit) { 821 | exit err; 822 | } 823 | 824 | # sort what's currently in the buffer 825 | outlen = mainsort(lines, count, patts, p, !!k); 826 | 827 | # if no temp file has been created, just dump the buffer and exit 828 | if (!tmpcount) { 829 | for (i=1; i<=outlen; i++) { 830 | print lines[i]; 831 | } 832 | 833 | exit err; 834 | } 835 | 836 | # if temp files have been created, merge them 837 | # write the last buffer to a temp file 838 | temps[++tmpcount] = mktemp("psort.XXXXXX"); 839 | for (i=1; i<=outlen; i++) { 840 | print lines[i] > temps[tmpcount]; 841 | } 842 | close(temps[tmpcount]); 843 | 844 | # get initial lines 845 | for (f=1; f<=tmpcount; f++) { 846 | if ((getline < temps[f]) > 0) { 847 | lines[f] = $0; 848 | 849 | # no lines left in the current file 850 | } else { 851 | finished[f] = 1; 852 | } 853 | } 854 | 855 | # loop continuously 856 | while (1) { 857 | # loop over each input file, and create a temp array 858 | unfinished = 0; 859 | len = 0; 860 | for (f=1; f<=tmpcount; f++) { 861 | if (!finished[f]) { 862 | cur_lines[++len] = lines[f]; 863 | # use the line as the index, to track which file it came from 864 | files_tmp[lines[f]] = f; 865 | 866 | 867 | # make sure there is at least one file left 868 | unfinished = 1; 869 | } 870 | } 871 | 872 | # if there are no lines left in any file, break the loop 873 | if (!unfinished) { 874 | break; 875 | } 876 | 877 | # sort the temp array 878 | cur_len = mainsort(cur_lines, len, patts, p, !!k); 879 | 880 | # print the first element in the sorted array 881 | print cur_lines[1]; 882 | 883 | # get the next line from the file the first element came from 884 | if ((getline < temps[files_tmp[cur_lines[1]]]) > 0) { 885 | lines[files_tmp[cur_lines[1]]] = $0; 886 | 887 | # no more lines for that file 888 | } else { 889 | finished[files_tmp[cur_lines[1]]] = 1; 890 | } 891 | } 892 | 893 | exit err; 894 | } 895 | 896 | 897 | 898 | # Copyright Daniel Mills 899 | # 900 | # Permission is hereby granted, free of charge, to any person obtaining a copy 901 | # of this software and associated documentation files (the "Software"), to 902 | # deal in the Software without restriction, including without limitation the 903 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 904 | # sell copies of the Software, and to permit persons to whom the Software is 905 | # furnished to do so, subject to the following conditions: 906 | # 907 | # The above copyright notice and this permission notice shall be included in 908 | # all copies or substantial portions of the Software. 909 | # 910 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 911 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 912 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 913 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 914 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 915 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 916 | # IN THE SOFTWARE. 917 | -------------------------------------------------------------------------------- /remove_old: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # removes all but the latest N files 4 | 5 | shopt -s extglob 6 | # default variable values 7 | to_keep=10 8 | to_prompt=0 9 | verbose=0 10 | dotfiles=0 11 | user_max=0 12 | has_max=1 13 | max=1 14 | follow=0 15 | force=0 16 | confirm=1 17 | 18 | usage() { 19 | cat <<'EOF' 20 | remove_old [OPTION]... [DIR]... 21 | 22 | Removes all but the latest N files in DIR(s). 23 | If no DIR is supplied, use current working directory. 24 | If DIR is '-', read list of dirs from stdin, one per line. 25 | 26 | Options: 27 | -h, --help Display this prompt and exit. 28 | -n, --num NUM Keep NUM files. default is 10. 29 | -r, --recurse Remove files recursively. 30 | -L, --follow Follow symlinks when recursing. By default, they will not 31 | be followed and instead will be ignored 32 | -m, --max NUM Only recurse NUM levels (implies -r) 33 | -f, --force Force removal of files (rm -f) 34 | -d, --dotfiles Remove dotfiles as well. 35 | -i, --interactive Prompt before removal of every file. 36 | -a, --no-confirm Do not display a confirmation prompt before removing. 37 | By default, a prompt is printed before processing each 38 | DIR 39 | -v, --verbose Display more info. 40 | 41 | Requires GNU find and sort. 42 | EOF 43 | } 44 | 45 | # usage: confirm PROMPT 46 | # yes/no prompt, returns according to the user's response 47 | confirm() { 48 | local -l reply 49 | 50 | read -p "$1 [y/N] " reply 51 | [[ $reply = y?(es) ]] 52 | } 53 | 54 | # usage: get_files DIR [FIND_OPTIONS] 55 | # run the find command, using the arguments beyond "$1" as options to find 56 | # determines whether or not to follow symlinks based on -f, --follow 57 | get_files() { 58 | if ((follow)); then 59 | find -L "$@" -type f -printf '%T@ %p\0' | sort -znr 60 | else 61 | find "$@" -type f -printf '%T@ %p\0' | sort -znr 62 | fi 63 | } 64 | 65 | # display error and exit with failure 66 | die() { 67 | printf '%s\n' "$@" >&2 68 | exit 1 69 | } 70 | 71 | # option string, for short options. 72 | # very much like getopts, any option followed by a ':' takes a required arg 73 | optstring=hn:rLm:fdiav 74 | 75 | # iterate over options, breaking -ab into -a -b and --foo=bar into --foo bar 76 | # also turns -- into --endopts to avoid issues with things like -o-, the '-' 77 | # should not be an end of options, but an invalid option 78 | unset options 79 | while (($#)); do 80 | case $1 in 81 | # if option is of type -ab 82 | -[!-]?*) 83 | # loop over each character starting with the second 84 | for ((i=1; i<${#1}; i++)); do 85 | c=${1:i:1} 86 | 87 | # add current char to options 88 | options+=("-$c") 89 | 90 | # if option takes a required argument, and it's not the last char 91 | # make the rest of the string its argument 92 | if [[ $optstring = *"$c:"* && ${1:i+1} ]]; then 93 | options+=("${1:i+1}") 94 | break 95 | fi 96 | done 97 | ;; 98 | # if option is of type --foo=bar, split on first '=' 99 | --?*=*) options+=("${1%%=*}" "${1#*=}");; 100 | # end of options, stop breaking them up 101 | --) 102 | options+=(--endopts) 103 | shift 104 | options+=("$@") 105 | break 106 | ;; 107 | # otherwise, nothing special 108 | *) options+=("$1");; 109 | esac 110 | 111 | shift 112 | done 113 | # set new positional parameters to altered options 114 | set -- "${options[@]}" 115 | unset options 116 | 117 | # parse options 118 | while [[ $1 = -?* ]]; do 119 | case $1 in 120 | -h|--help) usage >&2; exit 0;; 121 | -n|--num) 122 | # make sure NUM is a valid int 123 | [[ $2 && $2 != *[!0-9]* ]] || die "invalid number: $2" 124 | to_keep=$2 125 | shift 126 | ;; 127 | -r|--recurse) 128 | if ((!user_max)); then 129 | has_max=0 130 | fi 131 | ;; 132 | -L|--follow) follow=1;; 133 | -m|--max) 134 | # make sure NUM is a valid int 135 | [[ $2 && $2 != *[!0-9]* ]] || die "invalid number: $2" 136 | user_max=1 137 | has_max=1 138 | max=$2 139 | shift 140 | ;; 141 | -f|--force) force=1;; 142 | -d|--dotfiles) dotfiles=1;; 143 | -i|--interactive) to_prompt=1;; 144 | -a|--no-confirm) confirm=0;; 145 | -v|--verbose) verbose=1;; 146 | --endopts) shift; break;; 147 | *) die "invalid option: $1" 148 | esac 149 | 150 | shift 151 | done 152 | 153 | # populate array of dirs to use 154 | if (($#)); then 155 | dirs=() 156 | for dir; do 157 | # if target is '-', read list from stdin 158 | if [[ $dir = - ]]; then 159 | while read -r line; do 160 | dirs+=("$line") 161 | done 162 | 163 | # make stdin the terminal again for rm prompting 164 | exec &2 173 | fi 174 | done 175 | 176 | # if no dirs specified, use '.' 177 | else 178 | dirs=(.) 179 | fi 180 | 181 | # set rm command 182 | if ((to_prompt)); then 183 | rm=(rm -i) 184 | else 185 | rm=(rm) 186 | fi 187 | if ((force)); then 188 | rm+=(-f) 189 | fi 190 | 191 | # set recursion, whether or not to ignore dotfiles/dirs 192 | findopts=() 193 | if ((has_max)); then 194 | findopts+=(-maxdepth "$max") 195 | fi 196 | if ((!dotfiles)); then 197 | findopts+=(\( -name '.*' ! -name '.' \) -prune -o) 198 | fi 199 | 200 | # loop over each dir provided, remove old files 201 | for dir in "${dirs[@]}"; do 202 | if ((confirm)); then 203 | confirm "Remove all files except the $to_keep latest from $dir?" || 204 | continue 205 | fi 206 | if ((verbose)); then 207 | printf 'Processing dir: %s\n' "$dir" 208 | fi 209 | 210 | i=0 211 | while { read -rd ' ' t; IFS= read -rd '' f; } <&3; do 212 | if ((++i > to_keep)); then 213 | if ((verbose)); then 214 | printf 'Removing file: "%s" with mtime: "%s"\n' \ 215 | "$f" "$(date -d "@${t%.*}" +'%F %H:%M:%S')" 216 | fi 217 | 218 | "${rm[@]}" "$f" 219 | fi 220 | done 3< <(get_files "$dir" "${findopts[@]}") 221 | done 222 | 223 | 224 | 225 | # Copyright Daniel Mills 226 | # 227 | # Permission is hereby granted, free of charge, to any person obtaining a copy 228 | # of this software and associated documentation files (the "Software"), to 229 | # deal in the Software without restriction, including without limitation the 230 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 231 | # sell copies of the Software, and to permit persons to whom the Software is 232 | # furnished to do so, subject to the following conditions: 233 | # 234 | # The above copyright notice and this permission notice shall be included in 235 | # all copies or substantial portions of the Software. 236 | # 237 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 238 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 239 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 240 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 241 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 242 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 243 | # IN THE SOFTWARE. 244 | -------------------------------------------------------------------------------- /seg.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main(void) { 5 | printf("%s\n", NULL); 6 | return 0; 7 | } 8 | 9 | 10 | 11 | /* 12 | Copyright Daniel Mills 13 | 14 | Permission is hereby granted, free of charge, to any person obtaining a copy 15 | of this software and associated documentation files (the "Software"), to 16 | deal in the Software without restriction, including without limitation the 17 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 18 | sell copies of the Software, and to permit persons to whom the Software is 19 | furnished to do so, subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included in 22 | all copies or substantial portions of the Software. 23 | 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 27 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 29 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 30 | IN THE SOFTWARE. 31 | */ 32 | -------------------------------------------------------------------------------- /sub: -------------------------------------------------------------------------------- 1 | #!/usr/bin/awk -f 2 | 3 | # usage: sub -- [OPTIONS] SEARCH REPLACE [FILE(s)] 4 | # sub -- [OPTIONS] -f PATTERN_FILE [FILE(s)] 5 | # 6 | # The '--' is required, so AWK itself doesn't read the options 7 | # 8 | # In the 1st form, replace SEARCH with REPLACE in FILE(s), or stdin if no files 9 | # are provided or a target filename is '-'. 10 | # 11 | # In the 2nd form, read SEARCH and REPLACE strings from PATTERN_FILE, and 12 | # replace each SEARCH string with the appropriate REPLACE string. 13 | # The file format is as follows: 14 | # SEARCH 15 | # REPLACE 16 | # SEARCH 17 | # REPLACE 18 | # Each REPLACE string corresponds to the SEARCH string on the previous line. If 19 | # '-' is used as PATTERN_FILE, the list of patterns will be read from the 20 | # standard input and target FILEs are required. If there is an uneven number of 21 | # lines, the last SEARCH string will not be used. Empty SEARCH strings and their 22 | # corresponding REPLACE strings will be silently ignored. 23 | # 24 | # Options: 25 | # -f, --file FILE read SEARCH and REPLACE strings from FILE, instead of the 26 | # positional parameters. can be used multiple times for more 27 | # than one pattern file 28 | # -g, --global replaces all matches on each line, instead of just the first 29 | # -i, --in-place edit FILE(s) in place, instead of printing to stdout. this 30 | # option is ignored when reading the standard input 31 | # -h, --help display this help and exit 32 | # 33 | # SEARCH strings must be non-empty, but REPLACE strings may be empty to replace 34 | # SEARCH with nothing. (note that an argument (or line with -f) is still 35 | # required, but it may be the empty string) 36 | 37 | 38 | 39 | ## usage: getopts(optstring [, longopt_array ]) 40 | ## Parses options, and deletes them from ARGV. "optstring" is of the form 41 | ## "ab:c". Each letter is a possible option. If the letter is followed by a 42 | ## colon (:), then the option requires an argument. If an argument is not 43 | ## provided, or an invalid option is given, getopts will print the appropriate 44 | ## error message and return "?". Returns each option as it's read, and -1 when 45 | ## no options are left. "optind" will be set to the index of the next 46 | ## non-option argument when finished. "optarg" will be set to the option's 47 | ## argument, when provided. If not provided, "optarg" will be empty. "optname" 48 | ## will be set to the current option, as provided. Getopts will delete each 49 | ## option and argument that it successfully reads, so awk will be able to treat 50 | ## whatever's left as filenames/assignments, as usual. If provided, 51 | ## "longopt_array" is the name of an associative array that maps long options to 52 | ## the appropriate short option (do not include the hyphens on either). 53 | ## Sample usage can be found in the examples dir, with gawk extensions, or in 54 | ## the ogrep script for a POSIX example: https://github.com/e36freak/ogrep 55 | function getopts(optstring, longarr, opt, trimmed, hasarg, repeat) { 56 | hasarg = repeat = 0; 57 | optarg = ""; 58 | # increment optind 59 | optind++; 60 | 61 | # return -1 if the current arg is not an option or there are no args left 62 | if (ARGV[optind] !~ /^-/ || optind >= ARGC) { 63 | return -1; 64 | } 65 | 66 | # if option is "--" (end of options), delete arg and return -1 67 | if (ARGV[optind] == "--") { 68 | for (i=1; i<=optind; i++) { 69 | delete ARGV[i]; 70 | } 71 | return -1; 72 | } 73 | 74 | # if the option is a long argument... 75 | if (ARGV[optind] ~ /^--/) { 76 | # trim hyphens 77 | trimmed = substr(ARGV[optind], 3); 78 | # if of the format --foo=bar, split the two. assign "bar" to optarg and 79 | # set hasarg to 1 80 | if (trimmed ~ /=/) { 81 | optarg = trimmed; 82 | sub(/=.*/, "", trimmed); sub(/^[^=]*=/, "", optarg); 83 | hasarg = 1; 84 | } 85 | 86 | # invalid long opt 87 | if (!(trimmed in longarr)) { 88 | printf("unrecognized option -- '%s'\n", ARGV[optind]) > "/dev/stderr"; 89 | return "?"; 90 | } 91 | 92 | opt = longarr[trimmed]; 93 | # set optname by prepending dashes to the trimmed argument 94 | optname = "--" trimmed; 95 | 96 | # otherwise, it is a short option 97 | } else { 98 | # remove the hyphen, and get just the option letter 99 | opt = substr(ARGV[optind], 2, 1); 100 | # set trimmed to whatevers left 101 | trimmed = substr(ARGV[optind], 3); 102 | 103 | # invalid option 104 | if (!index(optstring, opt)) { 105 | printf("invalid option -- '%s'\n", opt) > "/dev/stderr"; 106 | return "?"; 107 | } 108 | 109 | # if there is more to the argument than just -o 110 | if (length(trimmed)) { 111 | # if option requires an argument, set the rest to optarg and hasarg to 1 112 | if (index(optstring, opt ":")) { 113 | optarg = trimmed; 114 | hasarg = 1; 115 | 116 | # otherwise, prepend a hyphen to the rest and set repeat to 1, so the 117 | # same arg is processed again without the first option 118 | } else { 119 | ARGV[optind] = "-" trimmed; 120 | repeat = 1; 121 | } 122 | } 123 | 124 | # set optname by prepending a hypen to opt 125 | optname = "-" opt; 126 | } 127 | 128 | # if the option requires an arg and hasarg is 0 129 | if (index(optstring, opt ":") && !hasarg) { 130 | # increment optind, check if no arguments are left 131 | if (++optind >= ARGC) { 132 | printf("option requires an argument -- '%s'\n", optname) > "/dev/stderr"; 133 | return "?"; 134 | } 135 | 136 | # set optarg 137 | optarg = ARGV[optind]; 138 | 139 | # if repeat is set, decrement optind so we process the same arg again 140 | # mutually exclusive to needing an argument, otherwise hasarg would be set 141 | } else if (repeat) { 142 | optind--; 143 | } 144 | 145 | # delete all arguments up to this point, just to make sure 146 | for (i=1; i<=optind; i++) { 147 | delete ARGV[i]; 148 | } 149 | 150 | # return the option letter 151 | return opt; 152 | } 153 | 154 | ## usage: lsub(str, repl [, in]) 155 | ## substites the string "repl" in place of the first instance of "str" in the 156 | ## string "in" and returns the result. does not modify the original string. 157 | ## if "in" is not provided, uses $0. 158 | function lsub(str, rep, val, len, i) { 159 | # if "in" is not provided, use $0 160 | if (!length(val)) { 161 | val = $0; 162 | } 163 | 164 | # get the length of val, in order to know how much of the string to remove 165 | if (!(len = length(str))) { 166 | # if "str" is empty, just prepend "rep" and return 167 | val = rep val; 168 | return val; 169 | } 170 | 171 | # substitute val for rep 172 | if (i = index(val, str)) { 173 | val = substr(val, 1, i - 1) rep substr(val, i + len); 174 | } 175 | 176 | # return the result 177 | return val; 178 | } 179 | 180 | ## usage: glsub(str, repl [, in]) 181 | ## behaves like lsub, except it replaces all occurances of "str" 182 | function glsub(str, rep, val, out, len, i, a, l) { 183 | # if "in" is not provided, use $0 184 | if (!length(val)) { 185 | val = $0; 186 | } 187 | # empty the output string 188 | out = ""; 189 | 190 | # get the length of val, in order to know how much of the string to remove 191 | if (!(len = length(str))) { 192 | # if "str" is empty, adds "rep" between every character and returns 193 | l = split(val, a, //); 194 | for (i=1; i<=l; i++) { 195 | out = out rep a[i]; 196 | } 197 | 198 | return out rep; 199 | } 200 | 201 | # loop while 'val' is in 'str' 202 | while (i = index(val, str)) { 203 | # append everything up to the search string, and the replacement, to out 204 | out = out substr(val, 1, i - 1) rep; 205 | # remove everything up to and including the first instance of str from val 206 | val = substr(val, i + len); 207 | } 208 | 209 | # append whatever is left in val to out and return 210 | return out val; 211 | } 212 | 213 | ## usage: mktemp(template [, type]) 214 | ## creates a temporary file or directory, safely, and returns its name. 215 | ## if template is not a pathname, the file will be created in ENVIRON["TMPDIR"] 216 | ## if set, otherwise /tmp. the last six characters of template must be "XXXXXX", 217 | ## and these are replaced with a string that makes the filename unique. type, if 218 | ## supplied, is either "f", "d", or "u": for file, directory, or dry run (just 219 | ## returns the name, doesn't create a file), respectively. If template is not 220 | ## provided, uses "tmp.XXXXXX". Files are created u+rw, and directories u+rwx, 221 | ## minus umask restrictions. returns -1 if an error occurs. 222 | function mktemp(template, type, 223 | c, chars, len, dir, dir_esc, rstring, i, out, out_esc, umask, 224 | cmd) { 225 | # portable filename characters 226 | c = "012345689ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 227 | len = split(c, chars, ""); 228 | 229 | # make sure template is valid 230 | if (length(template)) { 231 | if (template !~ /XXXXXX$/) { 232 | return -1; 233 | } 234 | 235 | # template was not supplied, use the default 236 | } else { 237 | template = "tmp.XXXXXX"; 238 | } 239 | 240 | # make sure type is valid 241 | if (length(type)) { 242 | if (type !~ /^[fdu]$/) { 243 | return -1; 244 | } 245 | 246 | # type was not supplied, use the default 247 | } else { 248 | type = "f"; 249 | } 250 | 251 | # if template is a path... 252 | if (template ~ /\//) { 253 | dir = template; 254 | sub(/\/[^/]*$/, "", dir); 255 | sub(/.*\//, "", template); 256 | 257 | # template is not a path, determine base dir 258 | } else { 259 | if (length(ENVIRON["TMPDIR"])) { 260 | dir = ENVIRON["TMPDIR"]; 261 | } else { 262 | dir = "/tmp"; 263 | } 264 | } 265 | 266 | # escape dir for shell commands 267 | esc_dir = dir; 268 | sub(/'/, "'\\''", esc_dir); 269 | esc_dir = "'" esc_dir "'"; 270 | 271 | # if this is not a dry run, make sure the dir exists 272 | if (type != "u" && system("test -d " esc_dir)) { 273 | return -1; 274 | } 275 | 276 | # get the base of the template, sans Xs 277 | template = substr(template, 0, length(template) - 6); 278 | 279 | # generate the filename 280 | do { 281 | rstring = ""; 282 | for (i=0; i<6; i++) { 283 | c = chars[int(rand() * len) + 1]; 284 | rstring = rstring c; 285 | } 286 | 287 | out_esc = out = dir "/" template rstring; 288 | sub(/'/, "'\\''", out_esc); 289 | out_esc = "'" out_esc "'"; 290 | } while (!system("test -e " out_esc)); 291 | 292 | # if needed, create the filename 293 | if (type == "f") { 294 | system("touch " out_esc); 295 | cmd = "umask"; 296 | cmd | getline umask; 297 | close(cmd); 298 | umask = substr(umask, 2, 1); 299 | system("chmod 0" 6 - umask "00 " out_esc); 300 | } else if (type == "d") { 301 | system("mkdir " out_esc); 302 | cmd = "umask"; 303 | cmd | getline umask; 304 | close(cmd); 305 | umask = substr(umask, 2, 1); 306 | system("chmod 0" 7 - umask "00 " out_esc); 307 | } 308 | 309 | # return the filename 310 | return out; 311 | } 312 | 313 | ## usage: shell_esc(string) 314 | ## returns the string escaped so that it can be used in a shell command 315 | function shell_esc(str) { 316 | gsub(/'/, "'\\''", str); 317 | 318 | return "'" str "'"; 319 | } 320 | 321 | # print usage info 322 | function usage() { 323 | printf("%s\n%s\n\n%s\n\n%s\n%s\n\n", 324 | "usage: sub -- [OPTIONS] SEARCH REPLACE [FILE(s)]", 325 | " sub -- [OPTIONS] -f PATTERN_FILE [FILE(s)]", 326 | "The '--' is required, so AWK itself doesn't read the options", 327 | "In the 1st form, replace SEARCH with REPLACE in FILE(s), or stdin if no files", 328 | "are provided or a target filename is '-'." \ 329 | ) > "/dev/stderr"; 330 | printf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n\n", 331 | "In the 2nd form, read SEARCH and REPLACE strings from PATTERN_FILE, and", 332 | "replace each SEARCH string with the appropriate REPLACE string.", 333 | "The file format is as follows:", 334 | " SEARCH", " REPLACE", " SEARCH", " REPLACE", 335 | "Each REPLACE string corresponds to the SEARCH string on the previous line. If", 336 | "'-' is used as PATTERN_FILE, the list of patterns will be read from the", 337 | "standard input and target FILEs are required. If there is an uneven number of", 338 | "lines, the last SEARCH string will not be used. Empty SEARCH strings and their", 339 | "corresponding REPLACE strings will be silently ignored." \ 340 | ) > "/dev/stderr"; 341 | printf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n\n", 342 | " Options:", 343 | " -f, --file FILE read SEARCH and REPLACE strings from FILE, instead of the", 344 | " positional parameters. can be used multiple times for more", 345 | " than one pattern file", 346 | " -g, --global replaces all matches on each line, instead of just the first", 347 | " -i, --in-place edit FILE(s) in place, instead of printing to stdout. this", 348 | " option is ignored when reading the standard input", 349 | " -h, --help display this help and exit" \ 350 | ) > "/dev/stderr"; 351 | printf("%s\n%s\n%s\n", 352 | "SEARCH strings must be non-empty, but REPLACE strings may be empty to replace", 353 | "SEARCH with nothing. (note that an argument (or line with -f) is still", 354 | "required, but it may be the empty string)" \ 355 | ) > "/dev/stderr"; 356 | } 357 | 358 | BEGIN { 359 | # initialize variables to defaults 360 | err = has_tmp = 0; 361 | global = in_place = has_patts = patterns = patts_from_stdin = 0; 362 | 363 | # map long options to short options 364 | longopts["file"] = "f"; 365 | longopts["global"] = "g"; 366 | longopts["in-place"] = "i"; 367 | longopts["help"] = "h"; 368 | 369 | # parse options 370 | while ((opt = getopts("f:gih", longopts)) != -1) { 371 | # -f, --file 372 | if (opt == "f") { 373 | # if FILE is '-', patterns are coming from stdin 374 | if (optarg = "-") { 375 | patts_from_stdin = 1; 376 | 377 | # otherwise, check to make sure it is a file or fifo and is readable 378 | } else { 379 | file = shell_esc(optarg); 380 | if ((system("test -f " file) && system("test -p " file)) || 381 | system("test -r " file)) { 382 | printf("%s: permission denied\n", optarg) > "/dev/stderr"; 383 | err = 1; exit; 384 | } 385 | } 386 | 387 | # read in each pair of lines from FILE, add to array 388 | while ((getline search < optarg) > 0 && (getline rep < optarg) > 0) { 389 | # search string may not be empty 390 | if (!length(search)) { 391 | continue; 392 | } 393 | 394 | patterns++; 395 | searches[patterns] = search; 396 | replaces[patterns] = search; 397 | } 398 | 399 | # do not use the positional parameters for the patterns 400 | has_patts = 1; 401 | break; 402 | 403 | # -g, --global 404 | } else if (opt == "g") { 405 | global = 1; break; 406 | 407 | # -i, --in-place 408 | } else if (opt == "i") { 409 | in_place = 1; break; 410 | 411 | # -h, --help 412 | } else if (opt == "h") { 413 | usage(); exit; 414 | 415 | # error 416 | } else { 417 | err = 1; exit; 418 | } 419 | } 420 | 421 | # if -f was used, make sure everything is valid 422 | if (has_patts) { 423 | # if no valid patterns were in the file(s), fail 424 | if (!patterns) { 425 | printf("no valid SEARCH/REPLACE pairs provided\n") > "/dev/stderr"; 426 | err = 1; exit; 427 | } 428 | 429 | # if stdin was used, make sure there are files given 430 | if (patts_from_stdin && optind >= ARGC) { 431 | printf("no input file\n") > "/dev/stderr"; 432 | err = 1; exit; 433 | } 434 | 435 | # get SEARCH and REPLACE from ARGV if -f was not used 436 | } else { 437 | # increment optind 438 | optind++; 439 | if (optind >= ARGC || !length(ARGV[optind-1])) { 440 | printf("no SEARCH or REPLACE string provided\n") > "/dev/stderr"; 441 | err = 1; exit; 442 | } 443 | 444 | # assign strings to array 445 | patterns++; 446 | searches[patterns] = ARGV[optind-1]; 447 | replaces[patterns] = ARGV[optind]; 448 | 449 | # delete the args 450 | for (i=1; i<=optind; i++) { 451 | delete ARGV[i]; 452 | } 453 | } 454 | } 455 | 456 | # before reading each input file, if -i was passed... 457 | FNR == 1 && in_place { 458 | # if it's not the first file, overwrite the previous file with its temp file 459 | if (has_tmp) { 460 | close(tmp); 461 | system("mv " shell_esc(tmp) " " shell_esc(FILENAME)); 462 | } 463 | 464 | # create temp file and add temp file name to the temps array 465 | tmp = mktemp(); 466 | temps[tmp]; 467 | 468 | has_tmp = 1; 469 | } 470 | 471 | # actually do the substitutions 472 | { 473 | # loop over each pattern pair 474 | for (p=1; p<=patterns; p++) { 475 | # substitute 476 | if (global) { 477 | $0 = glsub(searches[p], replaces[p]); 478 | } else { 479 | $0 = lsub(searches[p], replaces[p]); 480 | } 481 | } 482 | 483 | # print line, to stdout or temp file 484 | if (in_place) { 485 | print > tmp; 486 | } else { 487 | print; 488 | } 489 | } 490 | 491 | # clean up on exit, and exit according to "err" 492 | END { 493 | if (has_tmp) { 494 | # if -i was used, overwrite the last file with its temp file 495 | if (in_place) { 496 | close(tmp); 497 | system("mv " shell_esc(tmp) " " shell_esc(FILENAME)); 498 | } 499 | 500 | # clean up any remaining temp files 501 | for (tmp in temps) { 502 | system("rm -f " shell_esc(tmp)); 503 | } 504 | } 505 | 506 | exit err; 507 | } 508 | 509 | 510 | 511 | # Copyright Daniel Mills 512 | # 513 | # Permission is hereby granted, free of charge, to any person obtaining a copy 514 | # of this software and associated documentation files (the "Software"), to 515 | # deal in the Software without restriction, including without limitation the 516 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 517 | # sell copies of the Software, and to permit persons to whom the Software is 518 | # furnished to do so, subject to the following conditions: 519 | # 520 | # The above copyright notice and this permission notice shall be included in 521 | # all copies or substantial portions of the Software. 522 | # 523 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 524 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 525 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 526 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 527 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 528 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 529 | # IN THE SOFTWARE. 530 | -------------------------------------------------------------------------------- /update_gits: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # usage: update_gits [OPTIONS] [DIR ...] 4 | # 5 | # Find and update all git repositories (with git pull) within DIR, or the 6 | # current working directory if DIR is not provided. By default, only check for 7 | # repositories in the highest level sub-directories. Requires GNU find. 8 | # 9 | # Options: 10 | # -r, --recursive Search for repositories within DIR recursively 11 | # -m, --maxdepth DEPTH Limit recursiveness to DEPTH levels, just like GNU 12 | # find's -maxdepth option. Implies --recursive 13 | # -e, --exclude PATT Do not check directories matching PATT, and do not 14 | # descend into them when searching recursively. PATT is 15 | # a shell pattern, as used for find -name. May be 16 | # provided multiple times to exclude multiple patterns 17 | # -i, --ignore-case Make any instance of PATT case insensitive 18 | # -q, --quiet Only output for repos that are updated 19 | # -h, --help Display this help and exit 20 | 21 | 22 | 23 | # default global vars, shopts, etc 24 | shopt -s extglob nullglob 25 | recursive=0 26 | has_maxd=0 27 | maxdepth=1 28 | to_exclude=() 29 | icase=0 30 | quiet=0 31 | 32 | usage() { 33 | cat <<'EOF' 34 | usage: update_gits [OPTIONS] [DIR ...] 35 | 36 | Find and update all git repositories (with git pull) within DIR, or the 37 | current working directory if DIR is not provided. By default, only check for 38 | repositories in the highest level sub-directories. Requires GNU find. 39 | 40 | Options: 41 | -r, --recursive Search for repositories within DIR recursively 42 | -m, --maxdepth DEPTH Limit recursiveness to DEPTH levels, just like GNU 43 | find's -maxdepth option. Implies --recursive 44 | -e, --exclude PATT Do not check directories matching PATT, and do not 45 | descend into them when searching recursively. PATT is 46 | a shell pattern, as used for find -name. May be 47 | provided multiple times to exclude multiple patterns 48 | -i, --ignore-case Make any instance of PATT case insensitive 49 | -q, --quiet Only output for repos that are updated 50 | -h, --help Display this help and exit 51 | EOF 52 | } 53 | 54 | # option string, for short options. 55 | # very much like getopts, any option followed by a ':' takes a required arg 56 | optstring=rm:e:iqh 57 | 58 | # iterate over options, breaking -ab into -a -b and --foo=bar into --foo bar 59 | # also turns -- into --endopts to avoid issues with things like '-o-', the '-' 60 | # should not indicate the end of options, but be an invalid option (or the 61 | # argument to the option, such as wget -qO-) 62 | unset options 63 | while (($#)); do 64 | case $1 in 65 | -[!-]?*) 66 | for ((i=1; i<${#1}; i++)); do 67 | c=${1:i:1} options+=("-$c") 68 | 69 | if [[ $optstring = *"$c:"* && ${1:i+1} ]]; then 70 | options+=("${1:i+1}") 71 | break 72 | fi 73 | done 74 | ;; 75 | --?*=*) options+=("${1%%=*}" "${1#*=}");; 76 | --) 77 | options+=(--endopts); shift 78 | options+=("$@"); break 79 | ;; 80 | *) options+=("$1");; 81 | esac 82 | 83 | shift 84 | done 85 | set -- "${options[@]}" 86 | unset options 87 | 88 | # actually parse the options and do stuff 89 | while [[ $1 = -?* ]]; do 90 | case $1 in 91 | -r|--recursive) recursive=1;; 92 | -m|--maxdepth) 93 | if [[ -z $2 ]]; then 94 | printf 'No DEPTH provided for %s\n' "$1" >&2 95 | exit 1 96 | fi 97 | if [[ $2 = *[!0-9]* ]]; then 98 | printf 'Invalid DEPTH: %s\n' "$2" >&2 99 | exit 1 100 | fi 101 | recursive=1 102 | has_maxd=1 103 | maxdepth=$2 104 | shift 105 | ;; 106 | -e|--exclude) 107 | if [[ -z $2 ]]; then 108 | printf 'No PATT provided for %s\n' "$1" >&2 109 | exit 1 110 | fi 111 | to_exclude+=("$2") 112 | shift 113 | ;; 114 | -i|--ignore-case) icase=1;; 115 | -q|--quiet) quiet=1;; 116 | -h|--help) usage >&2; exit 0;; 117 | --endopts) shift; break;; 118 | *) printf 'Invalid option: %s\n' "$1" >&2; exit 1;; 119 | esac 120 | 121 | shift 122 | done 123 | 124 | # build the find options 125 | findopts=() 126 | if ((!recursive)); then 127 | findopts+=(-maxdepth 1) 128 | elif ((has_maxd)); then 129 | findopts+=(-maxdepth "$maxdepth") 130 | fi 131 | 132 | if ((${#to_exclude[@]})); then 133 | findopts+=(-type d '(') 134 | 135 | if ((icase)); then 136 | findopts+=(-iname "${to_exclude[0]}") 137 | else 138 | findopts+=(-name "${to_exclude[0]}") 139 | fi 140 | 141 | for ex in "${to_exclude[@]:1}"; do 142 | if ((icase)); then 143 | findopts+=(-o -iname "$ex") 144 | else 145 | findopts+=(-o -name "$ex") 146 | fi 147 | done 148 | 149 | findopts+=(')' -prune -o) 150 | fi 151 | 152 | findopts+=( 153 | -type d 154 | -exec test -f {}/.git/HEAD \; 155 | -exec sh -c 'cd "$1" && git rev-parse 2>/dev/null' _ {} \; 156 | -print0 157 | ) 158 | 159 | # run the find command, loop over each found directory 160 | while IFS= read -rd '' repo; do 161 | # update the repo 162 | ( 163 | if ! cd "$repo"; then 164 | printf 'Error changing into repo/directory: %s\n' "$repo" >&2 165 | exit 1 166 | fi 167 | 168 | if ((quiet)); then 169 | if ! out=$(git pull); then 170 | printf 'Error updating repo/directory: %s\n\n' "$repo" >&2 171 | exit 1 172 | fi 173 | if [[ $out != *"Already up-to-date"* ]]; then 174 | printf 'Updating repo/directory: %s\n' "$repo" 175 | printf '%s\n\n' "$out" 176 | fi 177 | 178 | else 179 | printf 'Updating repo/directory: %s\n' "$repo" 180 | if ! git pull; then 181 | printf 'Error updating repo/directory: %s\n\n' "$repo" >&2 182 | exit 1 183 | fi 184 | 185 | echo 186 | fi 187 | ) 188 | done < <(find "${@:-.}" "${findopts[@]}" 2>/dev/null) 189 | 190 | 191 | 192 | # Copyright Daniel Mills 193 | # 194 | # Permission is hereby granted, free of charge, to any person obtaining a copy 195 | # of this software and associated documentation files (the "Software"), to 196 | # deal in the Software without restriction, including without limitation the 197 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 198 | # sell copies of the Software, and to permit persons to whom the Software is 199 | # furnished to do so, subject to the following conditions: 200 | # 201 | # The above copyright notice and this permission notice shall be included in 202 | # all copies or substantial portions of the Software. 203 | # 204 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 205 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 206 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 207 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 208 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 209 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 210 | # IN THE SOFTWARE. 211 | -------------------------------------------------------------------------------- /url_decode.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main(int argc, char *argv[]) { 5 | /* check to make sure an arg is provided */ 6 | if (argc < 2) { 7 | fprintf(stderr, "%s\n", "no URL provided"); 8 | return 1; 9 | } 10 | 11 | /* decode the url, print */ 12 | printf("%s\n", curl_easy_unescape(NULL, argv[1], 0, NULL)); 13 | 14 | return 0; 15 | } 16 | 17 | 18 | 19 | /* 20 | Copyright Daniel Mills 21 | 22 | Permission is hereby granted, free of charge, to any person obtaining a copy 23 | of this software and associated documentation files (the "Software"), to 24 | deal in the Software without restriction, including without limitation the 25 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 26 | sell copies of the Software, and to permit persons to whom the Software is 27 | furnished to do so, subject to the following conditions: 28 | 29 | The above copyright notice and this permission notice shall be included in 30 | all copies or substantial portions of the Software. 31 | 32 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 33 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 34 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 35 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 36 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 37 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 38 | IN THE SOFTWARE. 39 | */ 40 | -------------------------------------------------------------------------------- /url_encode.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | /* sane URL length limit */ 7 | #define LEN 4096 8 | 9 | int main(int argc, char *argv[]) { 10 | char *p, out[LEN], cur[LEN]; 11 | int i=0; 12 | 13 | /* check to make sure an arg is provided */ 14 | if (argc < 2) { 15 | fprintf(stderr, "%s\n", "no URL provided"); 16 | return 1; 17 | } 18 | 19 | /* tokenize on '/' */ 20 | for (p = strtok(argv[1], "/"); p; p = strtok(NULL, "/")) { 21 | sprintf(cur, "%s", p); 22 | 23 | /* for first iteration, do not escape, and append an extra '/' if it 24 | ends with ':' to supply the protocol */ 25 | if (!i++) { 26 | if (cur[strlen(cur) - 1] == ':') { 27 | sprintf(out, "%s/", cur); 28 | } else { 29 | sprintf(out, "%s", p); 30 | } 31 | 32 | continue; 33 | } 34 | 35 | /* escape string, append to out */ 36 | sprintf(out, "%s/%s", out, curl_easy_escape(NULL, cur, 0)); 37 | } 38 | 39 | /* print the damn thing */ 40 | printf("%s\n", out); 41 | 42 | return 0; 43 | } 44 | 45 | 46 | 47 | /* 48 | Copyright Daniel Mills 49 | 50 | Permission is hereby granted, free of charge, to any person obtaining a copy 51 | of this software and associated documentation files (the "Software"), to 52 | deal in the Software without restriction, including without limitation the 53 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 54 | sell copies of the Software, and to permit persons to whom the Software is 55 | furnished to do so, subject to the following conditions: 56 | 57 | The above copyright notice and this permission notice shall be included in 58 | all copies or substantial portions of the Software. 59 | 60 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 61 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 62 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 63 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 64 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 65 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 66 | IN THE SOFTWARE. 67 | */ 68 | --------------------------------------------------------------------------------