#!/bin/bash . ScriptFunctions Import File Import GoboLinux Import OptionParser Parse_Conf Compile/Compile.conf helpOnNoArguments=yes scriptDescription="Perform all sorts of sanity checks in a recipe." scriptCredits="(C)2005-2006 Hisham Muhammad, released under the GNU GPL." scriptUsage="" scriptExample="Foo--1.0--recipe.tar.bz2" Add_Option_Boolean "t" "thorough" "Perform thorough URL/file testing (don't use cache)." Add_Option_Boolean "D" "quick-and-dirty" "cut some corners (don't download files, etc; not recommended)." Add_Option_Boolean "W" "no-web" "fully offline operation (not recommended)." Parse_Options "$@" [ "$httpSourceforge" ] || { echo -e "\033[41;33;1mFATAL ERROR: could not load initial configuration.\033[0m" errors=$[errors+1] exit 1 } if Boolean "no-web" then noweb=--no-web else noweb= fi workingdir=$PWD lintdir=`Temporary_Dir` function finish() { cd $workingdir rm -rf $lintdir if [ $errors -gt 0 -o $warnings -gt 0 ] then echo "$errors error(s), $warnings warning(s)." >> $report echo "$errors error(s), $warnings warning(s)." echo "Report in '$report'." [ $errors -gt 0 ] && exit 2 exit 1 else echo "Recipe looks sane." rm $report exit 0 fi } trap finish EXIT # need to get the output of utilities in English. export LANG=C export LC_ALL=C function msg() { echo "$@" echo "$@" >> $report } function WARN() { echo -e "\033[33;1mWarning: $@\033[0m" echo "Warning: $@" >> $report warnings=$[warnings+1] } function ERROR() { echo -e "\033[31;1mERROR: $@\033[0m" echo "ERROR: $@" >> $report errors=$[errors+1] } function FATAL() { echo -e "\033[41;33;1mFATAL ERROR: $@\033[0m" echo "FATAL ERROR: $@" >> $report errors=$[errors+1] exit 1 } errors=0 warnings=0 recipetar=$(readlink -f "$(Arg 1)") [ "$recipetar" ] || recipetar="$(Arg 1)" recipename=$(basename "$recipetar") report=$goboTemp/$(basename "$recipetar" .tar.bz2).report.txt touch "$report" &> /dev/null [ -w "$report" ] || { echo "No write permission for report file '$report'." exit 1 } :> $report echo msg "RecipeLint" msg "==========" msg "Checking $recipename on `date`" ############################################################################### msg "Checking recipe file..." [ -f "$recipetar" ] || { FATAL "$recipetar: Not a readable file." } file "$recipetar" | grep -q "bzip2 compressed data" || { FATAL "$recipetar: Not a bzip2 file." } ############################################################################### msg "Checking recipe package name..." name="${recipename%%--*}" version="${recipename#*--}" rest="${version#*--}" version="${version%%--*}" msg "Checking name for conventions..." [ "$rest" == "recipe.tar.bz2" ] || { FATAL "Invalid file name (does not end in --recipe.tar.bz2)." } [ "$name" == `NamingConventions $name` ] || { ERROR "Name $name does not follow naming conventions." } echo "$recipetar" | grep -q " " && { ERROR "Name or version contains spaces." } msg "Checking version number..." pureversion=`echo $version | sed 's/^\(.*\)-r[0-9]*$/\1/'` revision=`echo $version | sed -n 's/^.*-\(r[0-9]*\)$/\1/p'` echo "$pureversion" | egrep -q "^[0-9a-z_.]+$" || { ERROR "Version $pureversion contains invalid characters (only [0-9a-z._] allowed)." } echo "$pureversion" | grep -q "[0-9]\|^cvs$\|^svn$\|^git$\|^bzr$\|^hg$" || { WARN "Version $pureversion does not contain digits, may be incorrect." } [ "$revision" ] && { purerevision="${revision#r}" [ "$purerevision" -gt 0 ] || { ERROR "Invalid revision number $revision." } } || { # WARN "No revision id." : } ############################################################################### cd $lintdir tar jxpf "$recipetar" || { FATAL "Could not unpack. Corrupted file?" } msg "Checking recipe package structure..." [ -d "$name" ] || { FATAL "Can't find program directory inside tarball." } [ -d "$name/$version" ] || { FATAL "Can't find version directory inside tarball." } [ "`ls`" == "$name" ] || { WARN "Spurious files in root of recipe tarball: `ls | grep -v ^$name$`." } [ "`ls \"$name\"`" == "$version" ] || { WARN "Spurious files in package dir of recipe tarball: `ls \"$name\" | grep -v ^$version$`." } arches=() function check_dir() { local file for file in `ls "$2"` do case "$file" in Recipe) ;; Resources) ;; *.patch|*.patch.in) echo "$file" | egrep -q "^[0-9]+" || { [ `ls $2/*.patch | wc -l` -gt 1 ] && { ERROR "Patch file $file is not numbered; unreliable patching order." } } ;; i686|ppc|arm|sh4|x86_64|cell) [ "$1" = "root" ] && { arches=("${arches[@]}" $file) } || { WARN "Nested arch subdir $2/$file" } ;; *) WARN "Unknown file $file." ;; esac done } check_dir root "$name/$version" for arch in "${arches[@]}" do check_dir non-root "$name/$version/$arch" done ############################################################################### cd "$name/$version" msg "Checking recipe contents..." [ -f Recipe ] || { FATAL "No Recipe file." } msg "Performing basic syntax validation..." # Workaround weird bash behavior temprecipe=`Temporary_File` echo > $temprecipe cat ./Recipe >> $temprecipe bash -n $temprecipe || { rm $temprecipe FATAL "Recipe does not parse as valid shell script." } rm $temprecipe test_recipe=yes cat Recipe | perl -e ' %decls = ( is_compileprogram => "bool", is_makefile => "bool", is_perl => "bool", is_python => "bool", is_xmkmf => "bool", is_manifest => "bool", is_scons => "bool", is_meta => "bool", recipe_type => "string", compile_version => "string", environment => "array", url => "string", urls => "array", mirror_url => "string", mirror_urls => "array", file => "string", files => "array", file_size => "string", file_md5 => "string", file_sizes => "array", file_md5s => "array", uncompress => "string", unpack_files => "string", docs => "array", dir => "string", dirs => "array", include => "array", keep_existing_target => "bool", configure_options => "array", cmake_options => "array", autogen => "string", autogen_before_configure => "bool", build_variables => "array", install_variables => "array", make_variables => "array", configure => "string", make => "string", makefile => "string", build_target => "string", install_target => "string", do_build => "bool", do_install => "bool", needs_build_directory => "bool", needs_safe_linking => "bool", sandbox_options => "array", symlink_options => "array", cabal_options => "array", runhaskell => "string", manifest => "array", without => "array", create_dirs_first => "bool", python_options => "array", override_default_options => "bool", build_script => "string", cvs => "string", cvss => "array", cvs_module => "string", cvs_modules => "array", cvs_tag => "string", cvs_opts => "string", cvs_options => "string", cvs_checkout_options => "string", cvs_rsh => "string", cvs_password => "string", svn => "string", svns => "array", git => "string", gits => "array", bzr => "string", bzrs => "array", hg => "string", hgs => "array", unmanaged_files => "array", perl_options => "array", part_of => "string", post_install_message => "string" ); sub check_array_name { my ($name, $num) = @_; if (($decls{$name} ne "array") && !($name =~ /^with_[a-z0-9_]+$/)) { print "Error at line ".$num.": ".$name." is not a valid array.\n"; exit 1; } } sub check_var { my ($name, $value, $num) = @_; if ($decls{$name} eq "string") { } elsif ($decls{$name} eq "bool") { if ($value ne "yes" && $value ne "no") { print "Error at line ".$num.": ".$value." is not valid for ".$name." (expected yes or no).\n"; exit 1; } } elsif ($name =~ m/^with_[a-z0-9_]+$/) { } else { print "Error at line ".$num.": ".$name." is not a valid variable.\n"; exit 1; } } %functions = (); $in_shell = 0; $in_array = 0; $n = 0; while (<>) { $n++; if ($in_shell) { if (/^\}[[:blank:]]*$/) { $in_shell = 0; } else { # ignore shell contents } } elsif ($in_array) { if (/^[[:blank:]]*\)[[:blank:]]*$/) { $in_array = 0; } elsif (/^[[:blank:]]*(.*)$/) { $array_contents = $1; # TODO: check array contents } else { # TODO: check array contents } } elsif ($want_bracket) { $want_bracket = 0; if (/^[[:blank:]]*\{[[:blank:]]*$/) { $in_shell = 1; } else { print "Error at line ".$n.": expected a {\n"; print; exit 1; } } else { if (/^[[:blank:]]*#.*$/) { } elsif (/^[[:blank:]]*$/) { } elsif (/^((pre|post)_(patch|install|build|link)|private__[a-z0-9_]*)[[:blank:]]*\([[:blank:]]*\)[[:blank:]]*$/) { $fn_name = $1; if ($functions{$fn_name} == 1) { print "Error at line ".$n.": function redefinition:\n"; print; exit 1; } $functions{%fn_name} = 1; $want_bracket = 1; } elsif (/^((pre|post)_(patch|install|build|link)|private__[a-z0-9_]*|using_[a-z0-9_]+|do_(fetch|unpack|patch|configuration|build|install|symlink))[[:blank:]]*\([[:blank:]]*\)[[:blank:]]*\{/) { $fn_name = $1; if ($functions{$fn_name} == 1) { print "Error at line ".$n.": function redefinition:\n"; print; exit 1; } $functions{%fn_name} = 1; $in_shell = 1; } elsif (/^([a-z][a-z0-9_]*)=\((.*)\)/) { check_array_name($1, $n); $array_contents = $2; # TODO: check contents } elsif (/^([a-z][a-z0-9_]*)=\(/) { check_array_name($1, $n); $in_array = 1; } elsif (/^([a-z][a-z0-9_]*)=([^[:blank:]\n]*)[[:blank:]]*$/ || /^([a-z][a-z0-9_]*)="([^"]*)"[[:blank:]]*$/ || /^([a-z][a-z0-9_]*)='\''([^'\'']*)'\''[[:blank:]]*$/) { check_var($1, $2, $n); $var_contents = $2; # TODO: check contents } else { print "Error at line ".$n.": unexpected construct:\n"; print; exit 1; } } } if ($in_array) { print "Error at EOF: ) not found for array construct.\n"; print "Parser expects ) by itself in a line.\n"; exit 1; } if ($in_shell) { print "Error at EOF: } not found for shell construct.\n"; print "Parser expects } by itself in the beginning of a line.\n"; exit 1; } ' || { ERROR "Recipe does not pass basic validation. Will not perform recipe tests." test_recipe=no } test_urls() { function use_cached_file() { if [ "$cached" = "yes" ] then msg "Using cached file $file from $compileArchivesDir..." spider=--spider cp "$compileArchivesDir/$file" . fi } function get_file() { if Boolean "no-web" then use_cached_file return fi if ! Boolean "thorough" then use_cached_file fi if [ "$spider" ] then msg "Checking URL..." else msg "Downloading file ($file_size bytes)..." fi if ! wget --quiet $spider --timeout=20 --tries=3 "$url" then if [ "$mirror_url" ] then WARN "Primary URL unreacheable, trying the mirror instead..." wget --quiet $spider --timeout=20 --tries=3 "$mirror_url" fi fi } for i in `seq 0 $[${#urls[@]}-1]` do url="${urls[$i]}" file="${files[$i]}" file_md5="${file_md5s[$i]}" file_size="${file_sizes[$i]}" if [ "$file_md5" -o "$file_size" ] then full_check=yes spider= if [ "$file_size" = "0" ] then ERROR "Zero file size specified for $url." fi else full_check=no spider=--spider fi [ "$file" ] || file=`basename "$url"` Boolean "quick-and-dirty" && continue if [ -e "$compileArchivesDir/$file" ] then cached=yes else cached= fi if [ ! "$cached" ] && [ "$file_size" ] && [ "$file_size" -gt 5000000 ] && ! Boolean "thorough" then WARN "File is big, will not download (to force, run with --thorough)." full_check=no spider=--spider fi if get_file "$url" then if [ "$full_check" = "yes" ] then if [ -e "$file" ] then if [ ${file%.gz} != $file -o ${file%.tgz} != $file ] then msg "Testing integrity of archive $file..." gunzip --test $file || { ERROR "Downloaded file $file is corrupted." } elif [ ${file%.bz2} != $file -o ${file%.tbz2} != $file ] then msg "Testing integrity of archive $file..." bunzip2 --test $file || { ERROR "Downloaded file $file is corrupted." } elif [ ${file%.zip} != $file ] then msg "Testing integrity of archive $file..." unzip -t $file || { ERROR "Downloaded file $file is corrupted." } elif [ ${file%.lzma} != $file ] then msg "Testing integrity of archive $file..." lzma -t $file || { ERROR "Downloaded file $file is corrupted." } elif [ ${file%.xz} != $file ] then msg "Testing integrity of archive $file..." xz -t $file || { ERROR "Downloaded file $file is corrupted." } else WARN "Type of archive $file not detected. No integrity check performed." fi if [ "$file_md5" ] then msg "Checking MD5 for $file..." [ `md5sum "$file" | cut -d" " -f1` = "$file_md5" ] || { ERROR "MD5 sum in recipe for $file does not match." } else WARN "No MD5 sum in recipe." fi if [ "$file_size" ] then msg "Checking size for $file..." [ `wc -c "$file" | cut -d" " -f1` = "$file_size" ] || { ERROR "File size in recipe for $file does not match." } else WARN "No file size recipe." fi mv "$file" "$compileArchivesDir" else ERROR "Could not find downloaded file $file" fi else WARN "No MD5 or size -- full check on archive not performed." fi else ERROR "Unreachable URL: $url" fi done } test_recipe() { target="${goboPrograms}/$name/$version" settings_target="${goboPrograms}/$name/Settings" variable_target="${goboVariable}" source Recipe [ "$1" -a -e "$1/Recipe" ] && source "$1/Recipe" msg "Checking recipe declarations..." if [ ! "$recipe_type" ] then { if [ "$is_compileprogram" -o "$is_makefile" -o "$is_python" -o "$is_perl" \ -o "$is_xmkmf" -o "$is_meta" -o "$is_manifest" ] then WARN "Recipe type is declarated in deprecated style." # Backwards compability fix [ "$is_compileprogram" = "yes" ] && recipe_type="configure" [ "$is_makefile" = "yes" ] && recipe_type="makefile" [ "$is_python" = "yes" ] && recipe_type="python" [ "$is_perl" = "yes" ] && recipe_type="perl" [ "$is_xmkmf" = "yes" ] && recipe_type="xmkmf" [ "$is_meta" = "yes" ] && recipe_type="meta" [ "$is_scons" = "yes" ] && recipe_type="scons" [ "$is_manifest" = "yes" ] && recipe_type="manifest" else ERROR "Recipe type is not declared." fi } fi case "$recipe_type" in (configure | makefile | cmake | python | perl | xmkmf | meta | scons | manifest | cabal | waf) false;; esac && ERROR "Unknown Recipe type $recipe_type specified" [ "$recipe_type" = "meta" ] && { for inc in "${include[@]}" do msg "Checking include $inc..." echo "$inc" | grep -q -- "--" || { ERROR "Include $include does not have -- separator." continue } iname="${inc%%--*}" iversion="${inc#*--}" [ "$iname" == `NamingConventions $iname` ] || { WARN "Name $iname does not follow naming conventions." } echo "$iversion" | egrep -q "^[0-9a-z_.]+$" || { ERROR "Invalid characters in include version $iversion. Only [0-9a-z_.] allowed." } Boolean "quick-and-dirty" && continue msg "Checking availability of sub-recipe $iname $version..." FindPackage $noweb --type recipe "$iname" "$iversion" &> /dev/null || { ERROR "Required recipe $iname $iversion not found in repository." } done return } [ "$url" -o "${urls[0]}" -o "$cvs" -o "$svn" -o "$git" -o "$bzr" -o "$hg" ] || { ERROR "No download location specified in recipe." } if [ "$compile_version" ] then echo "$compile_version" | grep -qi "CVS" && { ERROR "Recipe references CVS version of Compile." } echo "$compile_version" | grep -qi "SVN" && { ERROR "Recipe references SVN version of Compile." } else WARN "Recipe has no compile_version declaration." fi if [ "$unpack_files" ] then case "$unpack_files" in inside_first|contents_inside_first) [ "${#urls[@]}" -gt 1 ] || { WARN "unpack_files=$unpack_files makes no sense with only one archive." } ;; dirs) [ "${#urls[@]}" -gt 1 ] || { WARN "unpack_files=dirs makes no sense with only one archive." } [ "${#urls[@]}" -ne ${#dirs[@]} ] || { ERROR "with unpack_files=dirs, number of entries in urls and dirs must be the same." } ;; files_in_root) ;; *) ERROR "Invalid value '$unpack_files' for unpack_files." ;; esac fi declarations=( configure_options=configure autogen_before_configure=configure autogen=configure build_variables=configure,makefile,cmake,xmkmf,scons,cabal,waf install_variables=configure,makefile,cmake,xmkmf,scons,cabal,waf make_variables=configure,makefile,xmkmf cmake_variables=cmake configure=configure makefile=configure,makefile,cmake,xmkmf build_target=configure,makefile,cmake,xmkmf,python,scons,waf install_target=configure,makefile,cmake,xmkmf,python,scons,waf do_build=configure,makefile,cmake,python,cabal,waf do_install=configure,makefile,cmake,xmkmf,python,scons,cabal,waf needs_build_directory=configure manifest=manifest without=perl create_dirs_first=configure,makefile,cmake,perl python_options=python override_default_options=python,scons,configure,cabal,waf build_script=python scons=scons scons_variables=scons cabal_options=cabal runhaskell=cabal waf=waf waf_options=waf waf_variables=waf ) for decl in "${declarations[@]}" do dname="${decl%%=*}" dtypes="${decl#*=}" dtypes="${dtypes//,/ }" if eval "[ \"\$$dname\" ]" then msg "Checking $dname..." eval "ok=no for mode in $dtypes do eval '[ \"\$$dname\" -a \"\$recipe_type\" == \"'\$mode'\" ] && { ok=yes; break; }' done [ \$ok = yes ] || { ERROR \"Declaration $dname not valid in this recipes' mode.\" }" fi done function check_vars() { aname="$1" shift while [ "$1" ] do echo "$1" | grep -q "=" || { ERROR "Entry '$1' in $aname does not look like a variable assignment." } vname="${1%%=*}" echo "$vname" | egrep -q "^[A-Za-z_][A-Za-z0-9_]*$" || { WARN "Entry '$vname' in $aname does not look like a variable name." } shift done } for vars in build_variables install_variables make_variables do eval "[ \"\${$vars[*]}\" ] && check_vars $vars \"\${$vars[@]}\"" done [ "${manifest[*]}" ] && { for mentry in "${manifest[@]}" do echo "$mentry" | grep -q ":" || { ERROR "Manifest entry '$mentry' is not of the format 'file:dir'." } done } urls=($url "${urls[@]}") files=($file "${files[@]}") file_md5s=($file_md5 "${file_md5s[@]}") file_sizes=($file_size "${file_sizes[@]}") [ "${urls[*]}" ] && test_urls msg "Looking for common error patterns..." grep -q '$goboPrograms' Recipe && { ERROR "Recipe references \$goboPrograms explicitly. Use \$target and \$_path." } # The obscure sed expression excludes url(s) variable from the check sed -e 's,#.*,,g' -e "/url/ {;/.*(/{;:loop;/.*)/b end;N; b loop;};:end;d}" Recipe | grep -q "/Programs" && { ERROR "Recipe references /Programs tree explicitly. Use \$target and \$_path." } grep -q "\$target/../Settings" Recipe && { WARN "Recipe uses old \$target/../Settings idiom. Use \$settings_target." } grep -q "\$target/../Variable" Recipe && { WARN "Recipe uses old \$target/../Variable idiom. Use \$variable_target." } sed 's,#.*,,g' Recipe | grep -q "/System[^\.]" && { ERROR "Recipe references /System tree explicitly. Use \$." } sed 's,#.*,,g' Recipe | grep "wget" | grep -q -v "url=\|urls=" && { ERROR "Recipe performs wget explicitly. Use the urls= array instead." } unset url unset file unset file_size unset file_md5 unset urls unset files unset file_sizes unset file_md5s } if [ "$test_recipe" = "yes" ] then grep -q "url=\|urls=\|cvs=\|svn=\|git=\|bzr=\|hg=" Recipe && test_recipe for arch in "${arches[@]}" do [ -e "$arch/Recipe" ] || continue grep -q "url=\|urls=\|cvs=\|svn=\|git=\|bzr=\|hg=" "$arch/Recipe" && test_recipe "$arch" done fi test_resources() { msg "Checking resources..." if [ "$part_of" ] then if [ "$noweb" ] then meta=`FindPackage --type recipe "$part_of" &> /dev/null` else meta=`GetRecipe "$part_of"` fi [ $? != 0 ] && ERROR "Meta-recipe $part_of not found in repository." Quiet pushd $meta fi [ -d Resources ] || { ERROR "No Resources directory." } [ -e Resources/PostInstall -a ! -x Resources/PostInstall ] && { ERROR "Resources/PostInstall must have its executable bit set." } msg "Checking Dependencies..." depfile=Resources/Dependencies if [ -f $depfile ] then grep -q "^# \*Warning\*" $depfile && { ERROR "Dependencies file contains unmatched library dependencies." } missing_deps() { CheckDependencies $noweb --types=$1 --mode=all --quiet-progress --no-recursive --no-blacklist $name $version recipe $PWD | while read d_program d_version d_type d_url; do if [ "$d_type" = "None" ] then if [ "$d_version" != "None" ] then echo \"$d_program $d_version\" else echo \"$d_program\" fi fi done } eval "no_nothing=(`missing_deps recipe,official_package`)" eval "no_recipe=(`missing_deps recipe`)" j=0 for prog in "${no_recipe[@]}" do noth="${no_nothing[$j]}" progname="${prog% *}" nothname="${noth% *}" if [ "$progname" = "$nothname" ] then if Boolean "no-web" then WARN "Dependency $prog not available locally (web access disabled)." else if ! wget -q -O /dev/null http://svn.gobolinux.org/recipes/revisions/$prog then ERROR "Dependency $prog not available." else WARN "Dependency $prog is not available for public access yet." fi fi j=$[j+1] else WARN "Dependency $prog has no recipe." fi done else [ ! "$part_of" ] && ERROR "No $depfile file." fi [ "$part_of" ] && Quiet popd if [ -f Resources/Description ] then if grep -ie '^\[License\] \(GNU \)\?\(GPL\|General Public Licen[cs]e\)$' Resources/Description then ERROR "No version of GPL specified in Resources/Description." fi else WARN "No Resources/Description file." fi } cat Recipe | grep -q "recipe_type=meta" || test_resources valid_vars=" Compile_goboDevices Compile_goboPrefix Compile_goboModules Compile_goboExecutables Compile_goboHeaders Compile_goboModules Compile_goboLibraries Compile_goboPrograms Compile_goboSettings Compile_goboTemp Compile_goboVariable Compile_goboData Compile_target Compile_settings_target Compile_variable_target " read_valid_dep_vars() { for i in `cat Resources/Dependencies | cut -d' ' -f1` do var=`echo $i | tr A-Z a-z | tr - _` valid_vars="$valid_vars Compile_$var""_path Compile_$var""_settings_path Compile_$var""_variable_path " done } test_patch() { patchfile=$1 msg "Checking patch ${patchfile}..." grep -q "/Programs" "${patchfile}" && { if [ $(basename "${patchfile}" .in) = "${patchfile}" ] then instruction="Rename to ${patchfile}.in and use @%Compile_target%@ and @%Compile__path%@." else instruction="Use @%Compile_target%@ and @%Compile__path%@." fi ERROR "Patch references /Programs tree explicitly. $instruction" } grep -q "/System" "${patchfile}" && { if [ $(basename "${patchfile}" .in) = "${patchfile}" ] then instruction="Rename to ${patchfile}.in and use @%Compile_%@." else instruction="Use @%Compile_%@." fi ERROR "Patch references /System tree explicitly. $instruction" } firstrow=$(head -n1 "${patchfile}") echo ${firstrow} | grep -q '^--- ' || echo ${firstrow} | grep -q '^diff ' && WARN "Patch file ${patchfile} lacks rationale. Please consider adding reason for patch to the top of the file." for i in `sed -r '/@%[^ %]+%@/!d' $1` do var=`echo $i | sed -r '/@%[^ %]+%@/!d' | sed -r 's/^.*@%([^ %]+)%@.*$/\1/g'` if [ -n "$var" ] then if [ -z "`echo $valid_vars | grep $var`" ] then ERROR "Patch uses invalid environment variable (@%$var%@)." fi fi done } read_valid_dep_vars ls *.patch *.patch.in 2> /dev/null | while read patch do test_patch "$patch" done