Skip to content

Instantly share code, notes, and snippets.

@proudzhu
Created June 10, 2013 18:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save proudzhu/5751193 to your computer and use it in GitHub Desktop.
Save proudzhu/5751193 to your computer and use it in GitHub Desktop.
#!/bin/bash
shopt -s extglob nullglob
nc=0
cron=0
edit=0
fullshebang=0
lang=bash
user_editor=0
editor=
toedit=()
names=()
# add a PATH assignment and export to the script, based on the curent PATH
# splits on colons so that the lines are truncated to 80 chars
add_path() {
local q=\' IFS=: cur=0 path len i elements
# split PATH (with escaped single quotes) into array on ":"
IFS=: read -ra path <<<"${PATH//$q/\\$q}"
elements=${#path[@]}
# iterate over array elements, adding the lengths until the line is too long
for ((i=0, len=0; i<=elements && len<=74; i++)); do
((len += ${#path[i]} + 1))
done
((i--))
# if a single element is too long, use it anyway
if ((cur < elements && cur == i)); then
((i++))
fi
# print the assignment, with all of the elements up to "i"
printf "PATH='%s'\n" "${path[*]:cur:i-cur}"
# cur is the element at which the line becomes too long
cur=$i
# continue looping over the indices, doing the same, until PATH is exhausted
until ((cur >= elements)); do
for ((i=cur, len=0; len<=72 && i<=elements; i++)); do
((len += ${#path[i]} + 1))
done
((i--))
if ((cur < elements && cur == i)); then
((i++))
fi
printf "PATH+=':%s'\n" "${path[*]:cur:i-cur}"
cur=$i
done
# print the export line
printf "%s\n\n" 'export PATH'
}
# usage: die MESSAGE
# print error and exit
die() {
printf '%s\n' "$@" >&2
exit 1
}
# usage: err MESSAGE
# print error, do not exit
err() {
printf '%s\n' "$@" >&2
}
# usage: editor FILE ...
# exec editor (if user supplied), VISUAL, EDITOR, or "vi", in that order
editor() {
if ((user_editor)); then
exec $editor "$@"
elif [[ $VISUAL ]]; then
exec $VISUAL "$@"
else
exec ${EDITOR:-vi} "$@"
fi
}
# usage: get_names IS_NUL
# reads stdin, appending each name within to the targets array. if IS_NUL is
# unset or empty, reads one name per line. otherwise, reads NUL-delimited names
get_names() {
local cmd=(read -r) name
if [[ $1 ]]; then
cmd+=(-d '')
fi
while IFS= "${cmd[@]}" name; do
names+=("$name")
done
}
# usage: noclobber FILE
# adds '.N' to a filename, sans extension, until it doesn't exist
noclobber() {
local out=$1 i=0
if [[ $1 = *+([^/]).*([^/]) ]]; then
local base=${1%.*} ext=${1##*.}
while [[ -e $out ]]; do
out=$base.$((++i)).$ext
done
else
while [[ -e $out ]]; do
out=$1.$((++i))
done
fi
printf '%s\n' "$out"
}
# usage: usage
# ...heh
usage() {
cat <<'EOF'
newscript [OPTIONS] NAME [...]
Creates a new script for each NAME, in the current working directory
Options:
-h, --help display this help and exit
-l, --language LANG specify the language LANG, default is bash
-s, --shebang SHEBANG provide the entire shebang as an arg, SHEBANG
-c, --cron add a your current PATH to the beginning of the
script. only available with -l bash (default), or sh
-n, --no-clobber prepends a number to the end of each existing NAME
instead of erring
-e, --edit open created files in a text editor
-p, --editor PROG use PROG instead of the default editor. by default,
VISUAL, EDITOR, or vi will be used, in that order.
implies -e (--edit)
-t, --files-from FILE get a list of NAMEs from FILE, one per line (if NAMEs
are also provided, both will be used)
-T, --files-from0 FILE same as --files-from, but uses NUL-delimited NAMEs
Available languages for LANG are:
bash, sh, awk, sed, perl, python
EOF
}
# iterate over options breaking -ab into -a -b when needed and
# --foo=bar into --foo bar
optstring=hl:s:cnep:t:T:
unset options
while (($#)); do
case $1 in
# if option is of type -ab
-[!-]?*)
# loop over each character starting with the second
for ((i=1; i<${#1}; i++)); do
c=${1:i:1}
# add current char to options
options+=("-$c")
# if option takes a required argument, and it's not the last char
# make the rest of the string its argument
if [[ $optstring = *"$c:"* && ${1:i+1} ]]; then
options+=("${1:i+1}")
break
fi
done
;;
# if option is of type --foo=bar, split on first '='
--?*=*) options+=("${1%%=*}" "${1#*=}");;
# end of options, stop breaking them up
--)
options+=(--endopts)
shift
options+=("$@")
break
;;
# otherwise, nothing special
*) options+=("$1");;
esac
shift
done
# set new positional parameters to altered options
set -- "${options[@]}"
unset options
# actually read the options and set stuff
while [[ $1 = -?* ]]; do
case $1 in
-h|--help) usage >&2; exit 0;;
-c|--cron) cron=1;;
-n|--no-clobber) nc=1;;
-l|--language)
[[ $2 ]] || die "LANG required for $1"
lang=$2
shift
;;
-s|--shebang)
[[ $2 ]] || die "SHEBANG required for $1"
fullshebang=1
shebang=$2
shift
;;
-e|--edit) edit=1;;
-p|--editor)
[[ $2 ]] || die "PROG required for $1"
user_editor=1
edit=1
editor=$2
;;
-t|--files-from)
[[ $2 ]] || die "FILE required for $1"
if [[ $2 = - ]]; then
get_names
elif [[ ! -f $2 || ! -r $2 ]]; then
die "$2 does not exist or is not readable"
else
get_names <"$2"
fi
shift
;;
-T|--files-from0)
[[ $2 ]] || die "FILE required for $1"
if [[ $2 = - ]]; then
get_names nul
elif [[ ! -f $2 || ! -r $2 ]]; then
die "$2 does not exist or is not readable"
else
get_names nul <"$2"
fi
shift
;;
--endopts) shift; break;;
*) die "invalid option: $1";;
esac
shift
done
# append remaining positional parameters to the names array
names+=("$@")
# if no names have been passed, either from the command line or if the
# --files-from file was empty, die
if ((!${#names[@]})); then
die "no targets specified"
fi
# if -s is used, check if the language is bash, sh, or perl, and set lang
if ((fullshebang)); then
case $shebang in
*/bash*([!/]) ) lang=bash;;
*/sh*([!/]) ) lang=sh;;
*/perl*([!/]) ) lang=perl;;
esac
# otherwise, validate lang and set the correct shebang
else
case $lang in
bash) shebang='#!/bin/bash';;
sh) shebang='#!/bin/sh';;
awk) shebang='#!/usr/bin/awk -f';;
sed) shebang='#!/usr/bin/sed -f';;
perl) shebang='#!/usr/bin/perl';;
python) shebang='#!/usr/bin/python';;
*) die "$lang is not an available preset language"
esac
fi
# make sure if cron is provided, language is bash or sh
if ((cron)) && [[ $lang != ?(ba)sh ]]; then
die "cron option is only available for bash or sh"
fi
# iterate over each target file
for file in "${names[@]}"; do
# if target exists
if [[ -e $file ]]; then
# if noclobber is on, set file to the new filename (with .N)
if ((nc)); then
file=$(noclobber "$file")
# otherwise, don't create the file, but try to add it to be edited for -e
else
err "$file already exists"
if [[ -f $file && -r $file ]]; then
toedit+=("$file")
fi
continue
fi
fi
# write the shebang, and possibly other stuff, to the new file
{
printf '%s\n\n' "$shebang"
case $lang in
bash)
printf '%s\n\n' 'shopt -s extglob nullglob'
((cron)) && add_path
;;
sh)
((cron)) && add_path
;;
perl)
printf '%s\n%s\n\n' 'use warnings;' 'use strict;'
;;
esac
} > "$file"
# make file executable
chmod +x -- "$file" || err "error making $file executable"
# add file to list of files to edit
toedit+=("$file")
done
# if -e is used, open the file(s) in the preferred editor
if ((edit));then
editor "${toedit[@]}"
fi
# Copyright Daniel Mills <dm@e36freak.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment