*! version 1.3.1 22dec2013 picard@netbox.com program define project /* -------------------------------------------------------------------------------- The main project program checks that only one project command is specified and reroutes each call to the appropriate local program. -------------------------------------------------------------------------------- */ // avoid version control but support version 9.2 to 13 if _caller() < 9.2 version 9.2 version `c(stata_version)' if c(stata_version) > 13 version 13 syntax [name(name=pname id="Project Name")], /// [ /// /// --------- project database ---------------- setup /// use a dialog to define a project's master do-file setmaster(string) /// define a project's master do-file plist /// list of projects and their directory pclear /// clear a project's record in the dataset of projects cd /// change Stata's dir to the project directory /// --------- project management tasks -------- build /// builds the project (runs the master do-file) list(string) /// list files in the project validate /// check for changes in files linked to the project replicate /// reruns a build and compare files created archive /// archive files that have changed since last build share(string) /// share files that have changed cleanup /// archive files that are not part of the project rmcreated /// erase all files created by the project /// --------- build directives ---------------- do(string) /// do-file to run original(string) /// do-file uses a file not created within the project uses(string) /// do-file uses a file created within the project relies_on(string) /// a related file not directly used (info, docs, etc.) creates(string) /// do-file creates a file doinfo /// returns info about the do-file and current build break /// to stop execution of a project at a specific point /// --------- command sub-options ------------- preserve /// preserve user data when running build directives TEXTlog /// log file in plain text format SMCLlog /// log file in SMCL format ] local command_list setup setmaster plist pclear cd /// build list validate replicate archive share cleanup rmcreated /// do original uses relies_on creates doinfo break local nopt 0 foreach opt in `command_list' { if "``opt''" ~= "" { local myopt `opt' local ++nopt } } if `nopt' > 1 { dis as err "options cannot be combined" exit 198 } if `nopt' == 0 { dis as err "no option specified" exit 198 } if "`textlog'" != "" & "`smcllog'" != "" { dis as err "options textlog and smcllog cannot be combined" exit 198 } project_`myopt' `0' end program define project_break /* -------------------------------------------------------------------------------- This command forces an error to stop execution of a build from within a do-file. This is functionally the same as writing -exit 1- in the do-file. -------------------------------------------------------------------------------- */ dis as err "project > user set break point" exit 1 end program define exit_if_in_a_build /* -------------------------------------------------------------------------------- Return an error if a build is currently running. This is used to stop execution for all project commands that are not allowed from within a build. When a build is started, a dataset is created to track its progress. This dataset is saved in the same directory as the program itself. This is done so that -project- can find it irrespective of the current directory. -------------------------------------------------------------------------------- */ // search along the current ado-path capture findfile "project.ado" local builtemp "`r(fn)'" local builtemp : subinstr local builtemp "project.ado" "project_BUILD_TEMPFILE.dta" // check using -describe- to avoid having to -preserve- user data cap describe using "`builtemp'" if !_rc { nobreak { use "`builtemp'", clear local pname : char _dta[pname] dis as err `"project "`pname'" is currently being built"' /// " - command not available" cap erase "`builtemp'" if _rc { dis as err "... could not erase build temporary file" dis as err `"you must manually delete "`builtemp'""' exit _rc } else dis as err "current build cancelled" clear } exit 198 } end program define project_setup /* -------------------------------------------------------------------------------- This command brings up a dialog where the user selects a master do-file. The do-file's name (minus the ".do" extension) will become the project name. The full path to the master do-file will then be added to the project database by the project_setmaster command. See the "project_setup.dlg". -------------------------------------------------------------------------------- */ // this is not a build directive exit_if_in_a_build // do not accept any option; can't specify the name of the project since // it will be overridden by the name of the master do-file syntax , setup db project_setup end program define project_pathname, rclass /* -------------------------------------------------------------------------------- Separate a file name from its path name. Return the full path name. -------------------------------------------------------------------------------- */ args fn // To avoid macro expansion problems local fn: subinstr local fn "\" "/", all // never allow temporary files tempfile f local tempdir : subinstr local f "\" "/", all local tempdir = regexr("`tempdir'","[^/]*$","") local fcheck : subinstr local fn "`tempdir'" "" local len1 : length local fcheck local len2 : length local fn if `len1' != `len2' { dis as err `"Temporary file not allowed: "`fn'""' exit 198 } // Navigate and build the path until we reach the filename. gettoken part rest : fn, parse("/:") while "`rest'" != "" { local path "`path'`part'" gettoken part rest : rest, parse("/:") } if inlist("`part'", "/", ":") { dis as err `"Was expecting a filename: "`fn'""' exit 198 } else { local fname "`part'" } // convert partial or relative path to full path by changing the // current directory and recovering the full path from there if "`path'" != "" { local savepwd "`c(pwd)'" nobreak { capture cd "`path'" if _rc { dis as err "invalid path or directory does not exist" dis as text "path = " as res "`path'" exit _rc } local fullp "`c(pwd)'" qui cd "`savepwd'" } } else { local fullp "`c(pwd)'" } // To avoid macro expansion problems local fullp: subinstr local fullp "\" "/", all display as result "`fullp'/`fname'" return local fullpath "`fullp'" return local fname "`fname'" // Check that the file exists confirm file "`fullp'/`fname'" end program define project_setmaster /* -------------------------------------------------------------------------------- This command will usually be called from the dialog that is brought up by the -project, setup- command (see the "project_setup.dlg"). It can also be called directly with a full or relative path. The project full path is stored in a Stata dataset that is saved in the directory that contains the version of -project- that is currently running. -------------------------------------------------------------------------------- */ // this is not a build directive exit_if_in_a_build syntax , setmaster(string) [TEXTlog SMCLlog] // Separate the filename from the file path qui project_pathname "`setmaster'" local fname "`r(fname)'" local fullpath "`r(fullpath)'" local len : length local fullpath // Check that we can store the file path if `len' > c(maxstrvarlen) { dis as txt "path = " as res `""`fullpath'""' dis as err "cannot store path; " /// "size = `len', limit = " c(maxstrvarlen) exit 1000 } // confirm that this is a do-file if regexm("`fname'","(.*)\.do$") local pname = regexs(1) else { dis as err "master do-file name must end with .do" exit 198 } // confirm that we have a valid project name capture confirm name `pname' if _rc | strpos("`pname'"," ") { dis as err `"Invalid project name: "`pname'""' dis as err "A short project name is recommended and it must conform to" dis as err "Stata's standard {help [M-1] naming:naming convention} for variables and other objects" exit 198 } local logtype = cond("`textlog'" == "","SMCL","plain text") preserve // Search along the ado-path for the dataset of projects capture findfile "project.dta" if !_rc { local projects "`r(fn)'" } else { // if not found, create it in the same directory as -project- capture findfile "project.ado" local projects "`r(fn)'" local projects : subinstr local projects "project.ado" "project.dta" clear gen str pname = "" gen str path = "" gen str plog = "" char pname[tname] "Name" char path[tname] "Full path to directory" char plog[tname] "Log Type" cap saveold "`projects'" if _rc { dis as err "Could not save the database of projects" exit _rc } } // Add the project cap use "`projects'", clear if _rc { dis as err "Could not open the database of projects" exit _rc } cap drop if pname == "`pname'" qui set obs `=_N+1' qui replace pname = "`pname'" in l qui replace path = "`fullpath'" in l // new feature; make backward compatible cap replace plog = "`logtype'" in l if _rc { gen str plog = "SMCL" // default for all projects char plog[tname] "Log Type" qui replace plog = "`logtype'" in l } sort pname cap saveold "`projects'", replace if _rc { dis as err "Could not save the dataset of projects" exit _rc } // list using our table routines qui keep if pname == "`pname'" rename path fpath rename pname fname project_table_setup fname plog, title1("Project") char fname[style] res project_table_line fname plog, line(1) dis "{c BLC}{hline `: char _dta[tablewidth]'}{c BRC}" end program define get_project_directory, rclass /* -------------------------------------------------------------------------------- Given a project name, this program retrieves the path to the directory that contains the master do-file. The program also returns the default log type for the project. -------------------------------------------------------------------------------- */ syntax name(name=pname id="Project Name") // Search along the ado-path for the dataset of projects capture findfile "project.dta" if _rc { dis as err "The dataset of projects was not found." dis as err "Use the {bf:{dialog project_setup:setup}} option " /// "to define new projects" exit _rc } cap use "`r(fn)'", clear if _rc{ dis as err "Could not open the database of projects" exit _rc } cap confirm string variable pname path if _rc{ dis as err "The database of projects is missing the expected variables" exit _rc } qui keep if pname == "`pname'" if _N == 1 { local projectdir = path return local projectdir "`projectdir'" return local pfiles "`projectdir'/`pname'_files.dta" return local plinks "`projectdir'/`pname'_links.dta" cap local plogtype = plog if "`plogtype'" == "" local plogtype "SMCL" return local plog "`plogtype'" } if "`projectdir'" == "" { dis as err `"project "`pname'" not found"' dis as err "Use the {bf:{dialog project_setup:setup}} option " /// "to define new projects" exit 198 } end program define project_plist /* -------------------------------------------------------------------------------- List one/all projects in the database of projects. -------------------------------------------------------------------------------- */ // this is not a build directive exit_if_in_a_build syntax [name(name=pname id="Project Name")], plist // Search along the ado-path for the dataset of projects cap findfile "project.dta" if _rc { dis as err "The dataset of projects was not found." dis as err "Use the {bf:{dialog project_setup:setup}} option " /// "to define new projects" exit _rc } local projects "`r(fn)'" preserve use "`projects'", clear if "`pname'" != "" { qui keep if pname == "`pname'" if !_N { dis as err `"project "`pname'" not found"' exit 459 } } if !_N { dis as err "no project defined" dis as err "Use the {bf:{dialog project_setup:setup}} option " /// "to define new projects" exit 2000 } // new feature; make backwards compatible cap confirm var plog if _rc { gen str plog = "SMCL" // default for all projects char plog[tname] "Log Type" sort pname cap saveold "`projects'", replace if _rc { dis as err "Could not save the dataset of projects" exit _rc } } // Adjust variable names and use our table routines rename path fpath rename pname fname if _N == 1 local title "Project" else local title "Projects" project_table_setup fname plog, title1("`title'") char fname[style] res sort fname forvalues i = 1/`c(N)' { project_table_line fname plog, line(`i') } dis "{c BLC}{hline `: char _dta[tablewidth]'}{c BRC}" end program define project_pclear /* -------------------------------------------------------------------------------- Remove a project in the database of projects. -------------------------------------------------------------------------------- */ // this is not a build directive exit_if_in_a_build syntax name(name=pname id="Project Name"), pclear // Search along the ado-path for the dataset of projects cap findfile "project.dta" if _rc { dis as err "The dataset of projects was not found." exit _rc } local projects "`r(fn)'" preserve use "`projects'", clear qui count if pname == "`pname'" if r(N) == 0 { dis as err `"project "`pname'" not found"' exit 459 } else { qui drop if pname == "`pname'" } sort pname cap saveold "`projects'", replace if _rc { dis as err "Could not save the dataset of projects" exit _rc } end program define project_cd, rclass /* -------------------------------------------------------------------------------- Change Stata's current working directory to the project's directory -------------------------------------------------------------------------------- */ // this is not a build directive exit_if_in_a_build syntax name(name=pname id="Project Name"), cd // Search along the ado-path for the dataset of projects cap findfile "project.dta" if _rc { dis as err "The dataset of projects was not found." exit _rc } local projects "`r(fn)'" preserve use "`projects'", clear qui keep if pname == "`pname'" if _N == 0 { dis as err `"project "`pname'" not found"' exit 459 } else { cd "`=path'" return local pwd "`=path'" } end program define project_build /* -------------------------------------------------------------------------------- Build a project. -------------------------------------------------------------------------------- */ // this is not a build directive; can't build from within a build! exit_if_in_a_build syntax name(name=pname id="Project Name"), build [TEXTlog SMCLlog] // restore to nothing if error/Break clear preserve get_project_directory `pname' local pdir "`r(projectdir)'" local pfiles "`r(pfiles)'" local plinks "`r(plinks)'" local plog "`r(plog)'" dis _n(2) as txt "Project directory : " as res "`pdir'" _n // Check the project's files and links databases and start // from clean versions if something's wrong cap noi check_project_files_links `pname' "`pfiles'" "`plinks'" if _rc { // post a warning only if we are overwriting existing files cap confirm file "`pfiles'" local overwrite = _rc == 0 cap confirm file "`plinks'" if `overwrite' | _rc == 0 dis /// as err "Warning: there is a problem with project's database of " /// as err "files and links. Starting a new build from scratch" // Create a new database of project files; one obs per file clear gen long fno = . // file number gen str fname = "" // file name gen str fpath = "" // file path gen byte relpath = . // flag if path is relative to project directory gen long csum = . // file's checksum (from -checksum-) gen double flen = . // file's length (from -checksum-) gen cvs = . // -checksum- version gen int cdate = . // date of last -checksum- call gen str ctime = "" // time of last -checksum- call gen int chngdate = . // date when added/last changed in project gen str chngtime = "" // time when added/last changed in project gen byte archiveflag = . // true if file should be archived // store a version in case we change the format in the future char _dta[files_version] 1 // labels for our printing routines char fname[tname] Filename char fpath[tname] "File Path " char csum[tname] Checksum char flen[tname] Length char chngdate[tname] Chng Date char chngtime[tname] ChngTime // how to convert from numeric to string; for our printing routines char flen[numconv] gen sflen = trim(string(flen, "%20.0fc")) char chngdate[numconv] gen schngdate = string(chngdate,"%d") char csum[numconv] gen scsum = string(csum,"%10.0f") // list of expected vars; for consistency checks char _dta[vlist_files] fno fname fpath relpath csum flen cvs /// cdate ctime chngdate chngtime archiveflag qui save "`pfiles'", replace emptyok // create a new database of links (dependencies) clear gen long fdo = . // fno of do-file gen long odo = . // fno of do-file that originated the link gen long flink = . // fno of the linked to file gen byte linktype = . // 1 original 2 uses 3 relies_on 4 creates gen long lkcsum = . // linked file's checksum (from -checksum-) gen double lkflen = . // linded file's length (from -checksum-) gen lkcvs = . // -checksum- version gen byte newlink = . // link comes from the current build gen int norder = . // link's order of appearance within the do-file gen byte level = . // nested do-file level // there are 4 types of links label def linktype_l 1 original 2 uses 3 relies_on 4 creates label values linktype linktype_l // label and numeric to string conversion for our printing routines char linktype[tname] Linktype char linktype[numconv] decode linktype, gen(slinktype) // list of expected vars; for consistency checks char _dta[vlist_links] fdo odo flink linktype lkcsum lkflen lkcvs /// newlink norder level qui save "`plinks'", replace emptyok } // use the project links dataset to track new links and the state of // the build qui use "`plinks'", clear if "`: char _dta[start_date]'" != "" { dis as text "Previous build start: " /// as res "`: char _dta[start_date]', `: char _dta[start_time]'" dis as text "Previous build end : " _cont if "`: char _dta[end_date]'" == "" { dis as err "unsuccessful" as text "" _n } else { dis as res "`: char _dta[end_date]', `: char _dta[end_time]'" _n(2) } } qui replace newlink = 0 local bdate "`c(current_date)'" local btime "`c(current_time)'" char _dta[start_date] "`bdate'" char _dta[start_time] "`btime'" char _dta[end_date] "" char _dta[end_time] "" qui save "`plinks'", replace emptyok // Create a dataset that we can find at any time and irrespective of the // current directory to indicate that we are currently building a project. // Put it in the same directory as the program itself. clear char _dta[pname] "`pname'" char _dta[pdir] "`pdir'" local d = date("`bdate'","`=cond(c(version) < 10,"dmy","DMY")'") char _dta[date] `d' char _dta[time] `btime' char _dta[plog] "`plog'" capture findfile "project.ado" local builtemp "`r(fn)'" local builtemp : subinstr local builtemp "project.ado" "project_BUILD_TEMPFILE.dta" qui save "`builtemp'", emptyok // restore the standard ado search path and drop all user programs global S_ADO `"`"UPDATES"';`"BASE"';`"SITE"';`"."';`"PERSONAL"';`"PLUS"';`"OLDPLACE"'"' program drop _all dis as text "Build start: " /// as res "`bdate', `btime'" as text "" dis as text _dup(`c(linesize)') "=" restore, not // cancel break hanling // run the master do-file but capture any error / Break key capture noi project_do , do("`pdir'/`pname'.do") `textlog' `smcllog' local build_rc = _rc // erase the build temporary file cap erase "`builtemp'" if `build_rc' { exit `build_rc' } clear preserve // break handling // record a successful build qui use "`plinks'", clear local bdate "`c(current_date)'" local btime "`c(current_time)'" char _dta[end_date] "`bdate'" char _dta[end_time] "`btime'" sort fdo norder qui save "`plinks'", replace dis as text _dup(`c(linesize)') "=" dis as text "Build successfully completed: " /// as res "`bdate'" /// as text ", " as res "`btime'" as text "" end program define project_replicate /* -------------------------------------------------------------------------------- Move all files created by the most recent build into a "replicate" directory within the project directory and run a replication build. Compare the files created against those in the "replicate" directory. This can only be done if the previous build was successful. -------------------------------------------------------------------------------- */ // this is not a build directive; can't build from within a build! exit_if_in_a_build syntax name(name=pname id="Project Name"), replicate [TEXTlog SMCLlog] // restore to nothing if error/Break clear preserve get_project_directory `pname' local pdir "`r(projectdir)'" local pfiles "`r(pfiles)'" local plinks "`r(plinks)'" local plog "`r(plog)'" dis _n(2) as txt "Project directory : " as res "`pdir'" _n // Check the project's files and links databases and start // from clean versions if something's wrong cap noi check_project_files_links `pname' "`pfiles'" "`plinks'" if _rc { dis as err "Problem with `pname'_files.dta or `pname'_links.dta;" /// " build project again" exit 459 } // stop if previous build was unsuccessful qui use "`plinks'", clear if "`: char _dta[end_date]'" == "" { dis as err "Previous build did not terminate normally" exit 459 } dis as text "Previous build start: " /// as res "`: char _dta[start_date]', `: char _dta[start_time]'" dis as text "Previous build end : " _cont dis as res "`: char _dta[end_date]', `: char _dta[end_time]'" _n // The master do-file is lined to all files in the most recent build. // The norder variable is kept to track the order in which each file // was created. qui keep if fdo == 1 qui keep if linktype == 4 keep flink norder rename flink fno sort fno qui merge fno using "`pfiles'" qui keep if _merge == 3 drop _merge rename csum csum00 rename flen flen00 rename cvs cvs00 sort fno tempfile fcreated qui save "`fcreated'" // Sort files by directory (ignore case) qui gen str Ufname = upper(fname) qui gen str Ufpath = upper(fpath) sort Ufpath Ufname local repdir "`pdir'/replicate" capture mkdir "`repdir'" dis _n as text "Backing up `c(N)' files to directory : " /// as res "`repdir'" _n local bpath "`repdir'" // move each file to the replicate directory forvalues i = 1/`c(N)' { // Traverse new path and create directories to the file if Ufpath[`i'] != Ufpath[`i'-1] { local fpath = fpath[`i'] local bpath "`repdir'" // File paths that are relative to the project directory follow // the same directory structure within the "replicate" directory. // Full file paths have to be completely created gettoken part fpath : fpath, parse("/:") while "`part'" != "" { if !inlist("`part'", "/", ":") { local bpath "`bpath'/`part'" capture mkdir "`bpath'" } gettoken part fpath : fpath, parse("/:") } } local fromp = fpath[`i'] local fname = fname[`i'] if "`fromp'" == "" local from "`fname'" else local from "`fromp'/`fname'" dis as res "`from'" if relpath[`i'] local from "`pdir'/`from'" local to "`bpath'/`fname'" capture copy "`from'" "`to'", replace if _rc { dis as err `"Could not copy "`from'" to "`to'""' exit _rc } qui erase "`from'" } // Initialize the replicate build. Drop all links to force a complete rebuild. qui use "`plinks'", clear qui drop if _n > 0 local bdate "`c(current_date)'" local btime "`c(current_time)'" char _dta[start_date] "`bdate'" char _dta[start_time] "`btime'" char _dta[end_date] "" char _dta[end_time] "" qui save "`plinks'", replace emptyok // Create a dataset that we can find at any time and irrespective of the // current directory to indicate that we are currently building a project. // Put it in the same directory as the program itself. clear char _dta[pname] "`pname'" char _dta[pdir] "`pdir'" local d = date("`bdate'","`=cond(c(version) < 10,"dmy","DMY")'") char _dta[date] `d' char _dta[time] `btime' char _dta[plog] "`plog'" capture findfile "project.ado" local builtemp "`r(fn)'" local builtemp : subinstr local builtemp "project.ado" "project_BUILD_TEMPFILE.dta" qui save "`builtemp'", emptyok // restore the standard ado search path and drop all user programs global S_ADO `"`"UPDATES"';`"BASE"';`"SITE"';`"."';`"PERSONAL"';`"PLUS"';`"OLDPLACE"'"' program drop _all dis _n as text "Replicate build start: " /// as res "`bdate', `btime'" as text "" dis as text _dup(`c(linesize)') "=" // run the master do-file but capture any error / Break key capture noi project_do , do("`pdir'/`pname'.do") `textlog' `smcllog' local build_rc = _rc // erase the build temporary file cap erase "`builtemp'" if `build_rc' { exit `build_rc' } qui use "`plinks'", clear local bdate "`c(current_date)'" local btime "`c(current_time)'" char _dta[end_date] "`bdate'" char _dta[end_time] "`btime'" sort fdo norder qui save "`plinks'", replace dis as text _dup(`c(linesize)') "=" dis as text "Replication build successfully completed: " /// as res "`bdate'" /// as text ", " as res "`btime'" as text "" // Check that files created match exactly the prior build qui keep if fdo == 1 qui keep if linktype == 4 keep flink norder rename flink fno sort fno qui merge fno using "`pfiles'" qui keep if _merge == 3 sort fno capture cf fno fpath fname norder using "`fcreated'" if _rc { dis as err "Files created by the project do no match" exit _rc } // combine post replication file info with pre-replication file info // for files created by the project. use "`pfiles'", clear sort fno qui merge fno using "`fcreated'" qui keep if _merge == 3 drop _merge // create a tempfile to save a modified copy of the log file tempfile fold // determine the name of Stata's temporary directory; // it's anything up to and including the last "/" or "\" character if regexm("`fold'","(.*[/\\])") local tempdir = regexs(1) dis as text _n /// "Checking files created vs. those from the previous build... " _n(2) gen chng = 0 sort norder tempfile flist qui save "`flist'" local nchng 0 forvalues i = 1/`c(N)' { local fpath = fpath[`i'] // determine path to archived file if "`fpath'" == "" local old_path "`repdir'" else { // full paths start with "/" local old_path = regexr("`fpath'","^/","") // DOS ":" is replaced with "/" local old_path = subinstr("`old_path'", ":","/",.) local old_path "`repdir'/`old_path'" } // path to the project file from the replicate build if relpath[`i'] local fpath "`pdir'/`fpath'" local fname = fname[`i'] local logsmcl = regexm("`fname'",".smcl$") local logtext = regexm("`fname'",".log$") // treat log file differently by ignoring a few things that change; // these are usually time stamp, tempfile related, or -save- output local chng 0 if `logsmcl' | `logtext' { qui infix str244 s0 1-244 using "`old_path'/`fname'", clear qui drop if regexm(s0,"filesig\([0-9]+:[0-9]+\)") qui drop if regexm(s0,"file .+ saved") qui drop if regexm(s0,"\(note: file .+ not found\)") qui replace s0 = subinstr(s0,"`tempdir'","",1) qui drop if regexm(s0,"project .+ > Build start :") qui drop if strpos(s0,"opened on: ") qui drop if strpos(s0,"closed on: ") qui drop if regexm(s0,"vars:.+[0-9]:[0-9][0-9]$") qui drop if regexm(s0,"obs:.+[0-9]:[0-9][0-9]$") qui drop if trim(s0) == "" qui compress qui save "`fold'", replace qui infix str244 s 1-244 using "`fpath'/`fname'", clear qui drop if regexm(s,"filesig\([0-9]+:[0-9]+\)") // project qui drop if regexm(s,"file .+ saved") // save qui drop if regexm(s,"\(note: file .+ not found\)") // save qui replace s = subinstr(s,"`tempdir'","",1) // tempfile name qui drop if regexm(s,"project .+ > Build start :") // project, doinfo qui drop if strpos(s,"opened on: ") // log open qui drop if strpos(s,"closed on: ") // log close qui drop if regexm(s,"vars:.+[0-9]:[0-9][0-9]$") // describe date/time qui drop if regexm(s,"obs:.+[0-9]:[0-9][0-9]$") // describe using date/time qui drop if trim(s) == "" qui compress qui merge using "`fold'" qui count if s != s0 local chng = r(N) } else { // Stata datasets are checked by data signatures since Stata // inserts new time stamps at every save. // Other files are compared using -checksum- capture use "`fpath'/`fname'", clear if _rc { use "`flist'", clear local chng = csum[`i'] != csum00[`i'] | /// flen[`i'] != flen00[`i'] | /// cvs[`i'] != cvs00[`i'] } else { qui version 9.2: datasignature, fast local newsig `r(datasignature)' qui use "`old_path'/`fname'", clear qui version 9.2: datasignature, fast local chng = "`r(datasignature)'" != "`newsig'" } } // if the created file is different, leave the copy in the replicate dir. qui use "`flist'", clear if `chng' { qui replace chng = 1 in `i' local ++nchng qui save "`flist'", replace } else { // erase the project file with no difference qui erase "`old_path'/`fname'" // remove empty directories local old_path : subinstr local old_path "`pdir'/" "" capture rmdir "`pdir'/`old_path'" while !_rc { local old_path = regexr("`old_path'", "/[^/]+$","") capture rmdir "`pdir'/`old_path'" } } } // log type is based on overall project setting and local option local logext = cond("`plog'" == "SMCL","smcl","log") if "`textlog'" != "" local logext "log" if "`smcllog'" != "" local logext "smcl" // prepare a report local d : dis %dCYND date("`bdate'","`=cond(c(version) < 10,"dmy","DMY")'") local t = subinstr("`btime'",":","",.) local logfile "replication_report_`d'_`t'.`logext'" capture mkdir "`repdir'" log using "`repdir'/`logfile'", name(replication_report) // capture break key/errors while logging cap noi { dis as txt dis as txt "Project name : " as res "`pname'" dis as txt "Project directory : " as res "`pdir'" dis as txt "Build completed : " as res "`bdate' `btime'" _n // use our table routines to list results qui gen status = cond(chng,"changed","same") char status[tname] " Status" // Setup the table local tablevars fname flen csum status chngdate chngtime project_table_setup `tablevars', /// title1("Replication Report") /// title2("`c(N)' files, in order they are created in the build") local tw : char _dta[tablewidth] sort norder forvalues i = 1/`c(N)' { local fp = fpath[`i'] local fn = fname[`i'] if "`fp'" == "" local ff "`fn'" else local ff "`fp'/`fn'" if relpath[`i'] local ff "`pdir'/`ff'" if chng[`i'] { char status[style] err } else { char status[style] txt } project_table_line `tablevars', line(`i') } dis "{c BLC}{hline `tw'}{c BRC}" // Final report if `nchng' == 0 { dis _n as text "No change found, project results are replicated" } else { dis _n as text "Number of differences found = " /// as res `nchng' as text "" } dis } // handle any error/break key local rc = _rc log close replication_report if `rc' exit `rc' end program define project_do /* -------------------------------------------------------------------------------- Run a do-file from within a build. The master do-file is called from -project_build- or -project_replicate-. Nested do-files are run when a -project, do()- build directive is encountered. -------------------------------------------------------------------------------- */ syntax , do(string) [preserve TEXTlog SMCLlog] // This is a build directive; check that we are currently running one capture findfile "project.ado" local builtemp "`r(fn)'" local builtemp : subinstr local builtemp "project.ado" "project_BUILD_TEMPFILE.dta" cap describe using "`builtemp'" if _rc { dis as err "no project being built" exit 198 } // restore to nothing if error/Break unless user wants its data back if "`preserve'" == "" clear preserve use "`builtemp'", clear local pname : char _dta[pname] local pdir : char _dta[pdir] local bdate : char _dta[date] local btime : char _dta[time] local plog : char _dta[plog] if "`pname'" == "" | "`pdir'" == "" | "`bdate'" == "" | "`btime'" == "" | "`plog'" == "" { dis as err "build parameters corrupted - this should never happen" exit 459 } local pfiles "`pdir'/`pname'_files.dta" local plinks "`pdir'/`pname'_links.dta" local prompt "project `pname' > " // Force the user to run do-files from within the project directory qui project_pathname "`do'" local dofile "`r(fname)'" local fullpath "`r(fullpath)'" if "`pdir'" == "`fullpath'" local dopath "" else local dopath : subinstr local fullpath "`pdir'/" "" local len_full : length local fullpath local len_rel : length local dopath if `len_full' == `len_rel' { dis as text "`prompt'" /// as err "do-file (`dopath'/`dofile') is not within the project directory" exit 119 } // Remove the ".do" file extension if regexm("`dofile'","(.*)\.do$") local dofstub = regexs(1) else { dis as text "`prompt'" /// as err "do-file (`dofile') must end with .do" exit 198 } // log type is based on overall project setting and local option local logext = cond("`plog'" == "SMCL","smcl","log") if "`textlog'" != "" local logext "log" if "`smcllog'" != "" local logext "smcl" local logfile = "`dofstub'.`logext'" // Find the do-file in the project files database. If the do-file is not // yet in the database, use the next available number; that's the number // that will be assigned by the dolink call just before running the do-file // for the first time qui use "`pfiles'", clear gen n = _n sum n if upper(fname) == upper("`dofile'") & /// upper(fpath) == upper("`dopath'"), meanonly if r(N) == 0 { sum fno, meanonly local fdo = cond(mi(r(max)), 1, r(max) + 1) } else { local fdo = fno[r(max)] } // Find the file number of the logfile if it exists sum n if upper(fname) == upper("`logfile'") & /// upper(fpath) == upper("`dopath'"), meanonly local flog = fno[r(max)] // Add the do-file to the list of currently active do-files. // Get the file number of the enclosing do-file, missing if this is the master qui use "`builtemp'", clear local dolist : char _dta[dolist] gettoken enclosing_do : dolist char _dta[dolist] `fdo' `dolist' qui save "`builtemp'", replace emptyok // Do not run the same do-file more than once. qui use "`plinks'", clear capture count if fdo == `fdo' & newlink if r(N) { dis as text "`prompt'" /// as err `"Cannot do "`dofile'" more than once per build"' exit 119 } // Another do-file should not be linked to this one qui count if flink == `fdo' & newlink if r(N) { dis as text "`prompt'" /// as err `"Another do-file is linked to "`dofile'""' exit 119 } // If this do-file successfully completed its previous run, it will have a // link to its logfile. If that's the case, we may not have to run it again. // Save copies of its previous links so that we may check. qui keep if fdo == `fdo' qui count if flink == `flog' local do_it = r(N) == 0 if !`do_it' { tempfile dolinks qui save "`dolinks'" } // Clear the links from the previous build qui use "`plinks'", clear qui drop if fdo == `fdo' qui save "`plinks'", replace emptyok if !`do_it' { // Check files linked to the do-file. qui use "`dolinks'", clear sort flink norder qui by flink: keep if _n == 1 keep flink lkcsum lkflen lkcvs rename flink fno qui merge fno using "`pfiles'" qui keep if _merge == 3 drop _merge // Any change means we must redo. qui count if lkcsum != csum | lkflen != flen | lkcvs != cvs local do_it = r(N) drop lkcsum lkflen lkcvs if !`do_it' { // For linked files that haven't been updated since the beginning of the // build, we must redo checksums to make sure they haven't changed. // Check the do-file first, then from smaller to larger files. // Stop at the first change. qui keep if cdate < `bdate' | (cdate == `bdate' & ctime <= "`btime'") gen update = 0 gen notdo = fno != `fdo' sort notdo flen local i 0 while !`do_it' & `i' < `c(N)' { local ++i local fpath = fpath[`i'] local fname = fname[`i'] if "`fpath'" == "" local f "`fname'" else local f "`fpath'/`fname'" if relpath[`i'] local f "`pdir'/`f'" capture checksum "`f'" if _rc { local do_it 1 } else { local d = date("`c(current_date)'","`=cond(c(version) < 10,"dmy","DMY")'") local t "`c(current_time)'" if csum[`i'] != r(checksum) | /// flen[`i'] != r(filelen) | /// cvs[`i'] != r(version) { qui replace chngdate = `d' in `i' qui replace chngtime = "`t'" in `i' qui replace archiveflag = 1 in `i' qui replace csum = r(checksum) in `i' qui replace flen = r(filelen) in `i' qui replace cvs = r(version) in `i' local do_it 1 } qui replace cdate = `d' in `i' qui replace ctime = "`t'" in `i' qui replace update = 1 in `i' } } // Save the updated checksums qui keep if update drop update notdo sort fno project_clear_dta_char qui merge fno using "`pfiles'" drop _merge sort fno qui save "`pfiles'", replace } } if `do_it' { // Move to the do-file's directory local savepwd "`c(pwd)'" qui cd "`fullpath'" // link the do-file (all all enclosing do-files) to itself project_dolink , linktype(1) linkfile("`dofile'") // Start the do-file with a clean slate project_clear_globals clear mata: mata clear timer clear program drop _all set seed 123456789 // Suspend log of the enclosing do-file and start a new one if "`enclosing_do'" != "" qui log off plog_`enclosing_do' capture log close plog_`fdo' log using "`logfile'", name(plog_`fdo') replace // cancel break handling, give user control if "`preserve'" != "" restore, preserve else restore, not // run the do-file; close the do-file if a break/error occured capture noisily do "`dofile'" // error/break handling local rc = _rc log close plog_`fdo' if "`enclosing_do'" != "" qui log on plog_`enclosing_do' if `rc' exit `rc' // link to the log file project_dolink , linktype(4) linkfile("`fullpath'/`logfile'") // restore the enclosing do-file's working directory qui cd "`savepwd'" } else { // Start from the links of a previous build. Keep only the // first instance for each linked file. This will indicate // the type of link. qui use "`dolinks'", clear sort flink norder qui by flink: keep if _n == 1 keep flink linktype rename linktype linktype0 tempfile flinks0 qui save "`flinks0'" qui count dis as text "`prompt'Skipping " as res "`dopath'/`dofile';" /// as text " no change in the " as res r(N) /// as text " files linked to it." // Since the master do-file is linked at this point to every // new link since the start of the build, check the validity // of the linktype values. qui use "`plinks'", clear qui keep if fdo == 1 if _N { sort flink norder qui by flink: keep if _n == 1 keep flink linktype sort flink qui merge flink using "`flinks0'" qui count if (linktype0 == 1 | linktype0 == 3) & linktype == 4 if r(N) { dis as text "`prompt'" /// as err "skipped do-file (or nested do-file within) contains" dis as text "`prompt'" /// as err "a -relies_on()- or -original() build directive but " /// "linked file is already created by the project" exit 119 } qui count if linktype0 == 2 & linktype != 4 if r(N) { dis as text "`prompt'" /// as err "skipped do-file (or nested do-file within) contains" dis as text "`prompt'" /// as err "a -uses()- build directive but " /// "no prior -creates()- build directive found" exit 119 } qui count if linktype0 == 4 & linktype != . if r(N) { dis as text "`prompt'" /// as err "skipped do-file (or nested do-file within) contains" dis as text "`prompt'" /// as err "a -creates()- build directive but" dis as text "`prompt'" /// as err "linked file is already in the project" exit 119 } } // Expand the links by the number of do-files currently running (at // this point `dolist' includes only enclosing do-files). use "`dolinks'", clear local nadd : word count `dolist' if `nadd' > 0 { gen n = _n qui expand `nadd' + 1 sort n qui by n: replace newlink = _n local i 1 foreach fdo in `dolist' { local ++i qui replace fdo = `fdo' if newlink == `i' } drop n } else { qui replace newlink = 1 } // Add these new links to the database. qui append using "`plinks'" // When we duplicated links for enclosing do-files, we gave them // newlink values of more than one. By sorting, we put them after // the current ones and then adjust norder. sort fdo newlink norder qui by fdo: replace norder = _n if newlink > 1 qui replace newlink = 1 if newlink > 1 sort fdo norder qui save "`plinks'", replace } // Remove the file number from the dolist qui use "`builtemp'", clear local dolist : char _dta[dolist] gettoken last rest : dolist char _dta[dolist] "`rest'" qui save "`builtemp'", replace emptyok end program define project_original /* -------------------------------------------------------------------------------- The original(filename) build directive is used to link the currently running do-file (and all upstream do-files) to a file that is not created by the project. The linked file is used in some way and therefore the results of the project could change (including log files) if the linked file changes. Therefore any change to the linked file will require that the do-file (and all upstream do-file) be rerun Because this is typically used just before inputting data (and therefore clearing the data in memory), the default is to not to preserve the use data while this program performs its functions. The -preserve- option can be used to overide the default behavior. -------------------------------------------------------------------------------- */ syntax , original(string) [preserve] // restore to nothing if error/Break unless user wants its data back if "`preserve'" == "" clear else { tempname hold cap estimates store `hold' } preserve project_dolink , linktype(1) linkfile("`original'") if "`preserve'" != "" cap estimates restore `hold' end program define project_uses /* -------------------------------------------------------------------------------- The uses(filename) build directive is used to link the currently running do-file (and all upstream do-files) to a file that was created by the project. The linked file is used in some way and therefore the results of the project could change (including log files) if the linked file changes. Therefore any change to the linked file will require that the do-file (and all upstream do-file) be rerun Note that is it not necessary to declare such a link in the do-file that actually creates the linked file. This is typically used with files that are created by a previously run do-file. Because this is typically used just before loading a Stata dataset (and therefore clearing the data in memory), the default is to not to preserve the use data while this program performs its functions. The -preserve- option can be used to overide the default behavior. -------------------------------------------------------------------------------- */ syntax , uses(string) [preserve] // restore to nothing if error/Break unless user wants its data back if "`preserve'" == "" clear else { tempname hold cap estimates store `hold' } preserve project_dolink , linktype(2) linkfile("`uses'") if "`preserve'" != "" cap estimates restore `hold' end program define project_relies_on /* -------------------------------------------------------------------------------- The relies_on(filename) build directive is used to link the currently running do-file (and all upstream do-files) to a file that is not created by the project. The linked file is NOT used by the do-file and therefore changes in the linked file would not alter results. However, the linked file's content is relevant to what the do-file does in some way. The linked file could be notes on how an original file was obtained, documentation about it, the algorithm used by the code, etc. While such links are strickly speaking not necessary, they are a good practice. The directive inserts in the log file the checksum of the linked file. If the linked file changes, this would mean that the do-file's log file would change and therefore the do-file must be rerun. An additional incentive/bonus to linking such files is that -project- knows that they are related to the project. The cleanup task will leave such files in place instead of moving them to an archive. Because this is typically used just before inputting data (and therefore clearing the data in memory), the default is to not to preserve the use data while this program performs its functions. The -preserve- option can be used to overide the default behavior. -------------------------------------------------------------------------------- */ syntax , relies_on(string) [preserve] // restore to nothing if error/Break unless user wants its data back if "`preserve'" == "" clear else { tempname hold cap estimates store `hold' } preserve project_dolink , linktype(3) linkfile("`relies_on'") if "`preserve'" != "" cap estimates restore `hold' end program define project_creates /* -------------------------------------------------------------------------------- The creates(filename) build directive is used to link the currently running do-file (and all upstream do-files) to a file that it just created. This can be a new Stata dataset created by a -save- statement but it is also for any file created by the project, e.g. -outsheet-, -outfile-, -graph-, -estimates save-... The default is to not to preserve the use data while this program performs its functions. The -preserve- option can be used to overide the default behavior. -------------------------------------------------------------------------------- */ syntax , creates(string) [preserve] // restore to nothing if error/Break unless user wants its data back if "`preserve'" == "" clear else { tempname hold cap estimates store `hold' } preserve project_dolink , linktype(4) linkfile("`creates'") if "`preserve'" != "" cap estimates restore `hold' end program define project_dolink, rclass /* -------------------------------------------------------------------------------- Link the currently running do-file to a file. This program is called from project_do, project_original, project_uses, project_relies_on, and project_creates. The calling programs handle -preserve- -------------------------------------------------------------------------------- */ syntax , linktype(integer) linkfile(string) // This is a build directive; check that we are currently running one capture findfile "project.ado" local builtemp "`r(fn)'" local builtemp : subinstr local builtemp "project.ado" "project_BUILD_TEMPFILE.dta" cap describe using "`builtemp'" if _rc { dis as err "no project being built" exit 198 } use "`builtemp'", clear local pname : char _dta[pname] local pdir : char _dta[pdir] local dolist : char _dta[dolist] local bdate : char _dta[date] local btime : char _dta[time] if "`pname'" == "" | "`pdir'" == "" | "`bdate'" == "" | "`btime'" == "" { dis as err "build parameters corrupted - this should never happen" exit 459 } local pfiles "`pdir'/`pname'_files.dta" local plinks "`pdir'/`pname'_links.dta" local prompt "project `pname' > " // Separate the filename from the file path qui project_pathname "`linkfile'" local fname "`r(fname)'" local fullpath "`r(fullpath)'" if "`pdir'" == "`fullpath'" local relpath "" else local relpath : subinstr local fullpath "`pdir'/" "" local len_full : length local fullpath local len_rel : length local relpath local use_relpath = `len_full' != `len_rel' // Check that we can store the file path and file name if `len_rel' > c(maxstrvarlen) { dis as txt "`prompt'path = " as res `""`relpath'""' dis as text "`prompt'" /// as err "cannot store path; " /// "size = `len_rel', limit = " c(maxstrvarlen) exit 1000 } if `: length local fname' > c(maxstrvarlen) { dis as txt "`prompt'filename = " as res `""`fname'""' dis as text "`prompt'" /// as err "cannot store filename; " /// "size = `: length local fname', limit = " c(maxstrvarlen) exit 1000 } // Find the file number of the linked file. If this is the first time, // add the file to the database qui use "`pfiles'", clear sum fno, meanonly local nextfno = cond(mi(r(max)), 1, r(max) + 1) gen n = _n sum n if upper(fname) == upper("`fname'") & /// upper(fpath) == upper("`relpath'"), meanonly local nobs = r(max) drop n local flink = fno[`nobs'] if mi(`flink') { local flink `nextfno' local nobs = _N + 1 qui set obs `nobs' qui replace fno = `flink' in `nobs' qui replace fpath = "`relpath'" in `nobs' qui replace fname = "`fname'" in `nobs' qui replace cdate = 0 in `nobs' qui replace relpath = `use_relpath' in `nobs' sort fno qui save "`pfiles'", replace } // We only need to recompute the checksum if this is a newly created // file or if the checksum was last computed before the start of the build tempname csum flen cvs if `linktype' == 4 | cdate[`nobs'] < `bdate' | /// (cdate[`nobs'] == `bdate' & ctime[`nobs'] < "`btime'") { qui checksum "`fullpath'/`fname'" scalar `csum' = r(checksum) scalar `flen' = r(filelen) scalar `cvs' = r(version) return add local d = date("`c(current_date)'","`=cond(c(version) < 10,"dmy","DMY")'") local t "`c(current_time)'" if csum[`nobs'] != `csum' | /// flen[`nobs'] != `flen' | /// cvs[`nobs'] != `cvs' { qui replace chngdate = `d' in `nobs' qui replace chngtime = "`t'" in `nobs' qui replace archiveflag = 1 in `nobs' qui replace csum = `csum' in `nobs' qui replace flen = `flen' in `nobs' qui replace cvs = `cvs' in `nobs' } qui replace cdate = `d' in `nobs' qui replace ctime = "`t'" in `nobs' sort fno qui save "`pfiles'", replace } else { scalar `csum' = csum[`nobs'] scalar `flen' = flen[`nobs'] scalar `cvs' = cvs[`nobs'] return scalar checksum = `csum' return scalar filelen = `flen' return scalar version = `cvs' } // Check against current links to see if it is appropriate to // link to this file. Use the master do-file as it is linked to all // files up to this point qui use "`plinks'", clear qui keep if fdo == 1 if _N { // If this is a link to an original file, make sure that the current // build has not created the file already. if `linktype' == 1 | `linktype' == 3 { qui count if flink == `flink' & linktype == 4 if r(N) { dis as text "`prompt'" /// as err "relies_on or original file is " /// "created by the project;" dis as text "`prompt'" /// as err `"try: project , uses(`anything') instead."' exit 119 } } // If the do-file uses a file created by the project, make sure that // the file has already been created. if `linktype' == 2 { qui count if flink == `flink' & linktype == 4 if r(N) == 0 { dis as text "`prompt'" /// as err "cannot link do-file to a file used;" dis as text "`prompt'" /// as err "file is not created by the project;" dis as text "`prompt'" /// as err `"try: project , original(`anything') instead."' exit 119 } } // If the do-file creates a file, make sure that the file is not // already in the project. Force a new checksum to be calculated // since the file is created after the initial build start. if `linktype' == 4 { qui count if flink == `flink' if r(N) { dis as text "`prompt'" /// as err "cannot link do-file to a file created;" dis as text "`prompt'" /// as err "file is already in the project;" exit 119 } } } // Add an observation for each do-file in the list. qui use "`plinks'", clear local ndo : word count `dolist' gettoken odo : dolist foreach fdo of numlist `dolist' { sum norder if fdo == `fdo', meanonly local n = cond(mi(r(max)), 1, r(max) + 1) local nobs = _N + 1 qui set obs `nobs' qui replace fdo = `fdo' in `nobs' qui replace odo = `odo' in `nobs' qui replace flink = `flink' in `nobs' qui replace linktype = `linktype' in `nobs' qui replace lkcsum = `csum' in `nobs' qui replace lkflen = `flen' in `nobs' qui replace lkcvs = `cvs' in `nobs' qui replace newlink = 1 in `nobs' qui replace level = `ndo' in `nobs' qui replace norder = `n' in `nobs' } // Save the links to disk. sort fdo norder qui save "`plinks'", replace // Display link info local linktype : word `linktype' of uses_original uses relies_on creates local linktype : subinstr local linktype "_" " " local scsum `: dis %12.0f `csum'' local sflen `: dis %17.0f `flen'' if "`relpath'" == "" local f "`fname'" else local f "`relpath'/`fname'" dis as text "`prompt'do-file `linktype': " as res `""`f'""' /// " filesig(`scsum':`sflen')" as text "" end program define project_doinfo, rclass /* -------------------------------------------------------------------------------- The doinfo build directive is used to get information about the current do-file and the status of the build. The default is to not to preserve the use data while this program performs its functions. The -preserve- option can be used to overide the default behavior. -------------------------------------------------------------------------------- */ syntax , doinfo [preserve] // This is a build directive; check that we are currently running one capture findfile "project.ado" local builtemp "`r(fn)'" local builtemp : subinstr local builtemp "project.ado" "project_BUILD_TEMPFILE.dta" cap describe using "`builtemp'" if _rc { dis as err "no project being built" exit 198 } // restore to nothing if error/Break unless user wants its data back if "`preserve'" == "" clear else { tempname hold cap estimates store `hold' } preserve use "`builtemp'", clear local pname : char _dta[pname] local pdir : char _dta[pdir] local dolist : char _dta[dolist] local bdate : char _dta[date] local bdate : dis %d `bdate' local btime : char _dta[time] if "`pname'" == "" | "`pdir'" == "" | "`bdate'" == "" | "`btime'" == "" { dis as err "build parameters corrupted - this should never happen" exit 459 } local pfiles "`pdir'/`pname'_files.dta" local plinks "`pdir'/`pname'_links.dta" local prompt "project `pname' > " dis as txt "`prompt'Project Name: " as res "`pname'" dis as txt "`prompt'Project Dir.: " as res "`pdir'" dis as txt "`prompt'Build start : " /// as res "`bdate', `btime'" as text "" gettoken current_do other_do : dolist qui use "`pfiles'", clear gen n = _n sum n if fno == `current_do', meanonly local nobs = r(min) local dofile = fname[`nobs'] if regexm("`dofile'","(.*)\.do$") local dofstub = regexs(1) dis as txt "`prompt'Do-file Name: " as res "`dofile'" if !mi("`other_do'") { dis as txt "`prompt'Enclosing do-files:" foreach fdo of numlist `other_do' { sum n if fno == `fdo', meanonly local nobs = r(min) local fname = fname[`nobs'] local fpath = fpath[`nobs'] dis as txt "`prompt' " _cont if !mi("`fpath'") dis as res "`fpath'/`fname'" else dis as res "`fname'" } } return local pname "`pname'" return local pdir "`pdir'" return local bdate "`bdate'" return local btime "`btime'" return local dofile "`dofstub'" if "`preserve'" != "" cap estimates restore `hold' end program define check_project_files_links /* -------------------------------------------------------------------------------- Perform consistency checks on the project's databases of files and links. Any problem encountered will require starting from scratch because we can't trust the information we have. This should never happen unless these files have been manually changed or if there is a bug in this program. -------------------------------------------------------------------------------- */ args pname pfiles plinks clear capture use "`pfiles'" if _rc { dis as err "`pfiles' not found or not in Stata format" exit _rc } if _N == 0 { dis as err "`pfiles' is empty" exit 2000 } if "`: char _dta[files_version]'" != "1" { dis as err "Format of `pfiles' has changed" exit 459 } local vlist : char _dta[vlist_files] unab vfound: * if `: list vlist == vfound' == 0 { dis as err "Variables in `pfiles' are different" exit 459 } if "`: sortedby'" != "fno" { dis as err "`pfiles' is not sorted by fno" exit 5 } capture by fno: assert _N == 1 if _rc { dis as err "fno is not unique in `pfiles'" exit 459 } sort fno if !(fname[1] == "`pname'.do" & fpath == "") | fno[1] != 1 { dis as err "bad fno for master do-file in `pfiles'" exit 459 } sort fpath fname capture by fpath fname: assert _N == 1 if _rc { dis as err "fpath fname is not unique in `pfiles'" exit 459 } clear capture use "`plinks'" if _rc { dis as err "`plinks' not found or not in Stata format" exit _rc } if _N == 0 { dis as err "`plinks' is empty" exit 2000 } local vlist : char _dta[vlist_links] unab vfound: * if !`: list vlist == vfound' { dis as err "Variables in `plinks' are different" exit 459 } if "`: sortedby'" != "fdo norder" { dis as err "`plinks' is not sorted by fdo norder" exit 5 } local start_time : char _dta[start_time] local badtime 1 if regexm("`start_time'", "^([0-9][0-9]):([0-9][0-9]):([0-9][0-9])$") { local hh = regexs(1) local mm = regexs(2) local ss = regexs(3) local badtime = `hh' > 60 | `mm' > 60 | `ss' > 60 } if `badtime' { dis as err "Bad or missing build start time in `plinks'" exit 459 } local start_date = date("`: char _dta[start_date]'","`=cond(c(version) < 10,"dmy","DMY")'") if mi(`start_date') { dis as err "Bad or missing build start date in `plinks'" exit 459 } capture by fdo norder: assert _N == 1 if _rc { dis as err "fno is not unique in `plinks'" exit 459 } if fdo[1] != 1 | flink[1] != 1 { dis as err "master do-file not the first link in `plinks'" exit 459 } // a file is created (4) before it is used (2) sort fdo flink norder capture by fdo flink: assert _n == 1 if linktype == 4 local myrc = _rc capture by fdo flink: assert linktype == 2 if linktype[1] == 4 & _n > 1 local myrc = `myrc' + _rc if `myrc' { dis as err "Inconsistent linktype" exit 459 } local end_time : char _dta[end_time] local build_ok 0 if regexm("`end_time'", "^([0-9][0-9]):([0-9][0-9]):([0-9][0-9])$") { local hh = regexs(1) local mm = regexs(2) local ss = regexs(3) local build_ok = `hh' <= 60 & `mm' <= 60 & `ss' <= 60 } local test = date("`: char _dta[end_date]'","`=cond(c(version) < 10,"dmy","DMY")'") if mi(`test') local build_ok 0 if `build_ok' { // The master do-file is linked to all files in the previous build qui keep if fdo == 1 keep flink sort flink qui by flink: keep if _n == 1 // Ignore old links, i.e. links from do-files that did not run in the most // recent build rename flink fdo qui merge fdo using "`plinks'" qui keep if _merge == 3 sort flink fdo capture by flink: assert lkcsum == lkcsum[1] & /// lkflen == lkflen[1] & lkcvs == lkcvs[1] if _rc { dis as err "Successful build but inconsistent checksums" exit 459 } } keep flink lkcsum lkflen lkcvs sort flink qui by flink: keep if _n == 1 rename flink fno sort fno qui merge fno using "`pfiles'" capture assert _merge != 1 if _rc { dis as err "`pname'_links.dta had links to files not in `pname'_files.dta" exit 459 } if `build_ok' { qui keep if _merge == 3 capture assert cdate > `start_date' | /// (cdate == `start_date' & ctime >= "`start_time'") if _rc { dis as err "Build ok but inconsistent dates/times for active files" exit 459 } capture assert lkcsum == csum & lkflen == flen & lkcvs == cvs if _rc { dis as err "Build ok but `pname'_links.dta and `pname'_files.dta" /// " checksums do not match" exit 459 } } end program define project_validate /* -------------------------------------------------------------------------------- This project task goes over all project files and checks that they haven't changed since the last build. If the previous build successfully terminated and none of the files have changed, then the build is validated. -------------------------------------------------------------------------------- */ // this is not a build directive exit_if_in_a_build syntax name(name=pname id="Project Name"), validate [TEXTlog SMCLlog] preserve get_project_directory `pname' local pdir "`r(projectdir)'" local pfiles "`r(pfiles)'" local plinks "`r(plinks)'" local plog "`r(plog)'" dis _n(2) as txt "Project directory : " as res "`pdir'" _n // Check the project's files and links databases cap noi check_project_files_links `pname' "`pfiles'" "`plinks'" if _rc { dis as err "Problem with `pname'_files.dta or `pname'_links.dta;" /// " build project again" exit 459 } // log type is based on overall project setting and local option local logext = cond("`plog'" == "SMCL","smcl","log") if "`textlog'" != "" local logext "log" if "`smcllog'" != "" local logext "smcl" // prepare date and time stamp local cdate "`c(current_date)'" local ctime "`c(current_time)'" local d : dis %dCYND date("`cdate'","`=cond(c(version) < 10,"dmy","DMY")'") local t = subinstr("`ctime'",":","",.) local datetime "`d'_`t'" // Display the previous build status qui use "`plinks'", clear dis as text "Build start: " /// as res "`: char _dta[start_date]', `: char _dta[start_time]'" dis as text "Build end : " _cont if "`: char _dta[end_date]'" == "" { dis as err "Previous build did not terminate normally" as text "" _n local buildok 0 } else { dis as res "`: char _dta[end_date]', `: char _dta[end_time]'" _n local buildok 1 } // The master do-file is linked to all the files in the project. Use it to // identify files in the project. qui use "`plinks'", clear qui keep if fdo == 1 keep flink sort flink qui by flink: keep if _n == 1 rename flink fno sort fno qui merge fno using "`pfiles'" qui drop if _merge == 2 // inactive files, not in most current build drop _merge qui gen status = "" char status[tname] " Status" // start a log file in the archive directory capture mkdir "`pdir'/archive" log using "`pdir'/archive/validate_`datetime'.`logext'", name(validate_log) // capture break key/errors while logging cap noi { // Setup the table local tablevars fname flen csum status chngdate chngtime project_table_setup `tablevars', /// title1("Alphabetical Index (`c(N)' files)") local tw : char _dta[tablewidth] // Show files in alphabetical order qui gen Ufname = upper(fname) qui gen Ufpath = upper(fpath) sort Ufname Ufpath // Choose variable styles char fname[style] res local ndif 0 forvalues i = 1/`c(N)' { local fpath = fpath[`i'] local fname = fname[`i'] if "`fpath'" == "" local ff "`fname'" else local ff "`fpath'/`fname'" if relpath[`i'] local ff "`pdir'/`ff'" capture checksum "`ff'" if _rc { qui replace status = "missing" in `i' char status[style] err local ++ndif } else if csum0[`i'] != r(checksum) | /// string version created by project_table_setup flen0[`i'] != r(filelen) | /// cvs[`i'] != r(version) { qui replace status = "changed" in `i' char status[style] err local ++ndif } else { qui replace status = "ok" in `i' char status[style] res } project_table_line `tablevars', line(`i') } dis "{c BLC}{hline `tw'}{c BRC}" // Final report if `ndif' == 0 & `buildok' { dis _n as text "No change found, project results are validated" } else { dis _n as text "Number of differences found = " /// as res `ndif' as text "" if !`buildok' dis as err "Incomplete build, results not validated" as text "" _n } dis } // handle any error/break key local rc = _rc log close validate_log if `rc' exit `rc' end program define project_archive /* -------------------------------------------------------------------------------- The archive task is used to make copies of all files that have been added to or have changed since the last time the archive task was performed. The task relies upon an archive flag to identify if a project file should be archived. If the previous build was unsuccessful, then the scope of this task is limited to files that were linked to at the time the build stopped. Files created by the project are not archived (under the assumption that they can be easily recreated). This is a good way to make a quick backup of what has changed since the last time the archive task was run. -------------------------------------------------------------------------------- */ // this is not a build directive exit_if_in_a_build syntax name(name=pname id="Project Name"), archive [TEXTlog SMCLlog] preserve get_project_directory `pname' local pdir "`r(projectdir)'" local pfiles "`r(pfiles)'" local plinks "`r(plinks)'" local plog "`r(plog)'" dis _n(2) as txt "Project directory : " as res "`pdir'" _n // Check the project's files and links databases cap noi check_project_files_links `pname' "`pfiles'" "`plinks'" if _rc { dis as err "Problem with `pname'_files.dta or `pname'_links.dta;" /// " build project again" exit 459 } // log type is based on overall project setting and local option local logext = cond("`plog'" == "SMCL","smcl","log") if "`textlog'" != "" local logext "log" if "`smcllog'" != "" local logext "smcl" // prepare date and time stamp local cdate "`c(current_date)'" local ctime "`c(current_time)'" local d : dis %dCYND date("`cdate'","`=cond(c(version) < 10,"dmy","DMY")'") local t = subinstr("`ctime'",":","",.) local datetime "`d'_`t'" // Display the previous build status qui use "`plinks'", clear local status : char _dta[end_date] dis as text "Previous build start: " /// as res "`: char _dta[start_date]', `: char _dta[start_time]'" dis as text "Previous build end : " _cont if "`status'" == "" { dis as err "unsuccessful" as text "" _n } else { dis as res "`: char _dta[end_date]', `: char _dta[end_time]'" _n } // The master do-file is linked to all files in the previous build qui keep if fdo == 1 sort flink norder qui by flink: keep if _n == 1 // Drop files that are created by the project, these can recreated qui drop if linktype == 4 // get file information for files linked to in the most recent build keep flink rename flink fno project_clear_dta_char qui merge fno using "`pfiles'" qui keep if _merge == 3 drop _merge // Target files that have changed since the last archive qui keep if archiveflag if _N { // create the archive directory capture mkdir "`pdir'/archive" local bdir "`pdir'/archive/archive_`datetime'" capture mkdir "`bdir'" if _rc { dis as err `"Could not create "`bdir'""' exit _rc } log using "`bdir'.`logext'", name(archive_log) // capture break key/errors while logging cap noi { local title "`c(N)' files moved to archive/archive_`datetime'" // Setup the table local tablevars fname flen csum chngdate chngtime project_table_setup `tablevars', title1("`title'") // Choose variable styles char fname[style] res // Sort files by directory (ignore case) qui gen Ufname = upper(fname) qui gen Ufpath = upper(fpath) sort Ufpath Ufname local bpath "`bdir'" // archive each file forvalues i = 1/`c(N)' { // Traverse new path and create directories to the file if Ufpath[`i'] != Ufpath[`i'-1] { local fpath = fpath[`i'] local bpath "`bdir'" gettoken part fpath : fpath, parse("/:") while "`part'" != "" { if !inlist("`part'", "/", ":") { local bpath "`bpath'/`part'" capture mkdir "`bpath'" } gettoken part fpath : fpath, parse("/:") } } local fromp = fpath[`i'] local fname = fname[`i'] if "`fromp'" == "" local from "`fname'" else local from "`fromp'/`fname'" if relpath[`i'] local from "`pdir'/`from'" local to "`bpath'/`fname'" capture copy "`from'" "`to'" if _rc { dis as err `"Could not copy "`from'" to "`to'""' exit _rc } project_table_line `tablevars', line(`i') } // Add a warning if the previous build was unsuccessful if "`status'" == "" { local tw : char _dta[tablewidth] dis "{c |}{space `tw'}{c |}" dis "{c |}" as err %~`tw's ">>> Incomplete Build <<<" as text "{c |}" } dis "{c BLC}{hline `: char _dta[tablewidth]'}{c BRC}" } // handle any error/break key local rc = _rc log close archive_log if `rc' exit `rc' // Update the file information keep fno archiveflag qui replace archiveflag = 0 sort fno qui merge fno using "`pfiles'" char _dta[archive_date] "`cdate'" char _dta[archive_time] "`ctime'" drop _merge order `: char _dta[vlist_files]' sort fno qui save "`pfiles'", replace } else { dis as text "No change since last archive" _n } end program define project_share /* -------------------------------------------------------------------------------- The share task is used to make copies of all files that have been added to or have changed since the last time project files were shared with the same person. This is similar to the archive task but is more flexible. If sharewith is left blank, then it is assumed to we are sharing with "me". If the previous build was unsuccessful, the date and time shared is not updated as this could create a situation where some files that are later in the build could be skipped the next time around. -------------------------------------------------------------------------------- */ // this is not a build directive exit_if_in_a_build syntax name(name=pname id="Project Name"), share(string) [TEXTlog SMCLlog] // reprocess the call using more specific syntax parsing local 0 `share' syntax [name(name=sharewith id="Sharing Name")], /// [ALLtime noCREated max(string) list] // give a name stub for the archive folder if no name is specified if "`sharewith'" == "" local sharewith "me" // file size max can be expressed in number of bytes, kB, MB, or GB local maxsize if regexm(trim("`max'"),"^[0-9]+$") local maxsize `max' if regexm(upper("`max'"),"([0-9]+) *B") local maxsize = regexs(1) if regexm(upper("`max'"),"([0-9]+) *K") local maxsize = regexs(1) + "000" if regexm(upper("`max'"),"([0-9]+) *M") local maxsize = regexs(1) + "000000" if regexm(upper("`max'"),"([0-9]+) *G") local maxsize = regexs(1) + "000000000" if "`maxsize'" == "" & "`max'" != "" { dis as err "invalid value for max(`max')" exit 198 } preserve get_project_directory `pname' local pdir "`r(projectdir)'" local pfiles "`r(pfiles)'" local plinks "`r(plinks)'" local plog "`r(plog)'" dis _n(2) as txt "Project directory : " as res "`pdir'" _n // Check the project's files and links databases cap noi check_project_files_links `pname' "`pfiles'" "`plinks'" if _rc { dis as err "Problem with `pname'_files.dta or `pname'_links.dta;" /// " build project again" exit 459 } // log type is based on overall project setting and local option local logext = cond("`plog'" == "SMCL","smcl","log") if "`textlog'" != "" local logext "log" if "`smcllog'" != "" local logext "smcl" // prepare date and time stamp local cdate "`c(current_date)'" local ctime "`c(current_time)'" local d : dis %dCYND date("`cdate'","`=cond(c(version) < 10,"dmy","DMY")'") local t = subinstr("`ctime'",":","",.) local datetime "`d'_`t'" // Display the previous build status qui use "`plinks'", clear local status : char _dta[end_date] dis as text "Previous build start: " /// as res "`: char _dta[start_date]', `: char _dta[start_time]'" dis as text "Previous build end : " _cont if "`status'" == "" { dis as err "unsuccessful" as text "" _n } else { dis as res "`: char _dta[end_date]', `: char _dta[end_time]'" _n } // The master do-file is linked to all files in the previous build qui keep if fdo == 1 sort flink norder qui by flink: keep if _n == 1 // Drop files that are created by the project is requested if "`created'" == "nocreated" qui drop if linktype == 4 // get file information for files linked to in the most recent build keep flink rename flink fno qui merge fno using "`pfiles'" qui keep if _merge == 3 drop _merge if "`maxsize'" != "" { qui drop if flen > `maxsize' } // get info on previous share local sharedate : char _dta[share_`sharewith'_date] local sharetime : char _dta[share_`sharewith'_time] local nsharedate = date("`sharedate'","`=cond(c(version) < 10,"dmy","DMY")'") if mi(`nsharedate') | "`alltime'" != "" local nsharedate 0 // Display last share time and date unless irrelevant if `nsharedate' != 0 & "`sharetime'" != "" & "`alltime'" == "" { dis as text "Last time shared with `sharewith' : " /// as res "`sharedate', `sharetime'" _n } qui keep if chngdate > `nsharedate' | /// (chngdate == `nsharedate' & chngtime > "`sharetime'") if _N { // create the archive project directory if it does not exists yet capture mkdir "`pdir'/archive" local bdir "`pdir'/archive/`sharewith'_`datetime'" if "`list'" == "" { capture mkdir "`bdir'" if _rc { dis as err `"Could not create "`bdir'""' exit _rc } } log using "`bdir'.`logext'", name(share_log) // capture break key/errors while logging cap noi { if "`list'" == "" local title "`c(N)' files copied to archive/`sharewith'_`datetime'" else local title "`c(N)' files to share with `sharewith'" // Setup the table local tablevars fname flen csum chngdate chngtime project_table_setup `tablevars', title1("`title'") // Choose variable styles char fname[style] res // Sort files by directory (ignore case) qui gen Ufname = upper(fname) qui gen Ufpath = upper(fpath) sort Ufpath Ufname local bpath "`bdir'" // archive each file forvalues i = 1/`c(N)' { if "`list'" == "" { // Traverse new path and create directories to the file if Ufpath[`i'] != Ufpath[`i'-1] { local fpath = fpath[`i'] local bpath "`bdir'" gettoken part fpath : fpath, parse("/:") while "`part'" != "" { if !inlist("`part'", "/", ":") { local bpath "`bpath'/`part'" capture mkdir "`bpath'" } gettoken part fpath : fpath, parse("/:") } } local fromp = fpath[`i'] local fname = fname[`i'] if "`fromp'" == "" local from "`fname'" else local from "`fromp'/`fname'" if relpath[`i'] local from "`pdir'/`from'" local to "`bpath'/`fname'" capture copy "`from'" "`to'" if _rc { dis as err `"Could not copy "`from'" to "`to'""' exit _rc } } project_table_line `tablevars', line(`i') } // Add a warning if the previous build was unsuccessful if "`status'" == "" { local tw : char _dta[tablewidth] dis "{c |}{space `tw'}{c |}" dis "{c |}" as err %~`tw's ">>> Incomplete Build <<<" as text "{c |}" } dis "{c BLC}{hline `: char _dta[tablewidth]'}{c BRC}" } // handle any error/break key local rc = _rc log close share_log if `rc' exit `rc' // update shared date if "`list'" == "" { if "`status'" == "" { dis as err _n "Since the last build is imcomplete, " /// `"share date and time with "`sharewith'" not updated"' } else { qui use "`pfiles'", clear char _dta[share_`sharewith'_date] "`c(current_date)'" char _dta[share_`sharewith'_time] "`c(current_time)'" qui save "`pfiles'", replace } } } else { dis as text "No change since last share with `sharewith'" _n } end program define project_cleanup /* -------------------------------------------------------------------------------- This program scans all files in the project directory and in all subdirectories recursively and archives files that are not linked to the project. -------------------------------------------------------------------------------- */ // this is not a build directive exit_if_in_a_build syntax name(name=pname id="Project Name"), cleanup [TEXTlog SMCLlog] preserve get_project_directory `pname' local pdir "`r(projectdir)'" local pfiles "`r(pfiles)'" local plinks "`r(plinks)'" local plog "`r(plog)'" dis _n(2) as txt "Project directory : " as res "`pdir'" _n // Check the project's files and links databases cap noi check_project_files_links `pname' "`pfiles'" "`plinks'" if _rc { dis as err "Problem with `pname'_files.dta or `pname'_links.dta;" /// " build project again" exit 459 } // log type is based on overall project setting and local option local logext = cond("`plog'" == "SMCL","smcl","log") if "`textlog'" != "" local logext "log" if "`smcllog'" != "" local logext "smcl" // prepare date and time stamp local cdate "`c(current_date)'" local ctime "`c(current_time)'" local d : dis %dCYND date("`cdate'","`=cond(c(version) < 10,"dmy","DMY")'") local t = subinstr("`ctime'",":","",.) local datetime "`d'_`t'" // Display the previous build status qui use "`plinks'", clear if "`: char _dta[end_date]'" == "" { dis as err "Previous build did not terminate normally" exit 459 } dis as text "Previous build start: " /// as res "`: char _dta[start_date]', `: char _dta[start_time]'" dis as text "Previous build end : " _cont dis as res "`: char _dta[end_date]', `: char _dta[end_time]'" _n // The master do-file is linked to all files in the previous build qui keep if fdo == 1 keep flink sort flink qui by flink: keep if _n == 1 rename flink fno project_clear_dta_char qui merge fno using "`pfiles'" qui keep if _merge == 3 drop _merge // add the project files and links databases to the list local nobs = _N + 1 qui set obs `nobs' qui replace fname = "`pname'_files.dta" in `nobs' local nobs = _N + 1 qui set obs `nobs' qui replace fname = "`pname'_links.dta" in `nobs' // Make sure that the archive directory exist capture mkdir "`pdir'/archive" local bdir "`pdir'/archive/cleanup_`datetime'" capture mkdir "`bdir'" if _rc { dis as err `"Could not create "`bdir'""' exit _rc } log using "`bdir'.`logext'", name(cleanup_log) // capture break key/errors while logging cap noi { dis as txt _n "List of unused files moved to " /// as res "archive/cleanup_`datetime'" _n // recursively clean-up the project directory and subdirectories project_cleanupdir , currentdir(".") pdir("`pdir'") bdir("`bdir'") dis } // handle any error/break key local rc = _rc log close cleanup_log if `rc' exit `rc' // Save the project files database with the inactive files removed // Only files that are linked to in the previous build remain. qui drop if fname == "`pname'_files.dta" qui drop if fname == "`pname'_links.dta" sort fno qui save "`pfiles'", replace // go back and clean-up old links keep fno rename fno fdo sort fdo project_clear_dta_char qui merge fdo using "`plinks'" qui keep if _merge == 3 drop _merge sort fdo norder qui save "`plinks'", replace end program define project_cleanupdir /* -------------------------------------------------------------------------------- This program is called initially with the currentdir set at "." (which indicates the project's main directory) and travels down each subdirectory recursively to compare all files against the list of files in the project (dataset in memory). -------------------------------------------------------------------------------- */ syntax , currentdir(string) pdir(string) bdir(string) [list] if "`currentdir'" == "." { local currentdir "`pdir'" local relpath "" } else { local relpath : subinstr local currentdir "`pdir'/" "" } // get a list of all files in the current directory local flist: dir "`currentdir'" files "*" local i 0 foreach f of local flist { // Is this file in the project? If not, we'll move it out qui count if upper(fname) == upper("`f'") & /// upper(fpath) == upper("`relpath'") & relpath local doit = r(N) == 0 // ignore OS X's folder attribute hidden file if "`f'" == ".DS_Store" local doit 0 if `doit' { local ++i local from = "`currentdir'/`f'" dis as txt "`from'" if mi("`list'") { // Create the directory if this is the first file if `i' == 1 { // start at the base backup directory local bpath "`bdir'" // travel each part of the relative path to create dir // note that since we are somewhere within the project // directory, all paths are relative to "`pdir'". No // need to check for DOS ":" drive character. local fpath "`relpath'" gettoken part fpath : fpath, parse("/") while "`part'" != "" { if "`part'" != "/" { local bpath "`bpath'/`part'" capture mkdir "`bpath'" } gettoken part fpath : fpath, parse("/") } } local to = "`bpath'/`f'" capture copy "`from'" "`to'" if _rc { dis as err `"Could not copy "`from'" to "`to'""' exit _rc } capture erase "`from'" if _rc { dis as err `"Could not erase "`from'" "' exit _rc } } } } // get a list of directories in the current directory local dlist: dir "`currentdir'" dirs "*" // remove the archive directory if we are in the project's main directory if "`pdir'" == "`currentdir'" /// local dlist : subinstr local dlist `""archive""' "" // continue the cleanup by travelling down each directory foreach d of local dlist { project_cleanupdir , currentdir("`currentdir'/`d'") /// pdir("`pdir'") bdir("`bdir'") `list' if mi("`list'") { // get a list of all files in the cleaned-up directory local flist: dir "`currentdir'/`d'" files "*" // if all that's left is OS X's folder attribute hidden file if `"`flist'"' == `"".DS_Store""' /// capture erase "`currentdir'/`d'/.DS_Store" // remove an empty directory capture rmdir "`currentdir'/`d'" if _rc == 0 dis as txt "Removed directory " /// as res "`currentdir'/`d'" } } end program define project_rmcreated /* -------------------------------------------------------------------------------- This program removes all files created by the project in the previous build. -------------------------------------------------------------------------------- */ // this is not a build directive exit_if_in_a_build syntax name(name=pname id="Project Name"), rmcreated [TEXTlog SMCLlog] preserve get_project_directory `pname' local pdir "`r(projectdir)'" local pfiles "`r(pfiles)'" local plinks "`r(plinks)'" local plog "`r(plog)'" dis _n(2) as txt "Project directory : " as res "`pdir'" _n // Check the project's files and links databases cap noi check_project_files_links `pname' "`pfiles'" "`plinks'" if _rc { dis as err "Problem with `pname'_files.dta or `pname'_links.dta;" /// " build project again" exit 459 } // log type is based on overall project setting and local option local logext = cond("`plog'" == "SMCL","smcl","log") if "`textlog'" != "" local logext "log" if "`smcllog'" != "" local logext "smcl" // prepare date and time stamp local cdate "`c(current_date)'" local ctime "`c(current_time)'" local d : dis %dCYND date("`cdate'","`=cond(c(version) < 10,"dmy","DMY")'") local t = subinstr("`ctime'",":","",.) local datetime "`d'_`t'" // Display the previous build status qui use "`pdir'/`pname'_links.dta", clear dis as text "Previous build start: " /// as res "`: char _dta[start_date]', `: char _dta[start_time]'" dis as text "Previous build end : " _cont if "`: char _dta[end_date]'" == "" { dis as err "unsuccessful" as text "" _n } else { dis as res "`: char _dta[end_date]', `: char _dta[end_time]'" _n } // The master do-file is linked to all files in the previous build qui use "`plinks'", clear qui keep if fdo == 1 & linktype == 4 rename flink fno sort fno qui merge fno using "`pfiles'" qui keep if _merge == 3 // Sort files by directory (ignore case) qui gen Ufname = upper(fname) qui gen Ufpath = upper(fpath) sort Ufpath Ufname if _N { // start a log file in the archive directory capture mkdir "`pdir'/archive" log using "`pdir'/archive/rmcreated_`datetime'.`logext'", name(rmcreated_log) // capture break key/errors while logging cap noi { dis as text "Erasing `c(N)' files ... " forvalues i = 1/`c(N)' { local fp = fpath[`i'] local fn = fname[`i'] if "`fp'" == "" local f "`fn'" else local f "`fp'/`fn'" if relpath[`i'] local f "`pdir'/`f'" erase "`f'" dis as res "`f'" } } // handle any error/break key local rc = _rc log close rmcreated_log if `rc' exit `rc' } else { dis as text "No file created by the project found" } end program define project_list /* -------------------------------------------------------------------------------- List project files in various ways -------------------------------------------------------------------------------- */ // this is not a build directive exit_if_in_a_build syntax name(name=pname id="Project Name"), list(string) [TEXTlog SMCLlog] // More than one option is ok. Check if option is available. local opts build type index directory concordance archive cleanup local bad : list list - opts if "`bad'" != "" { dis as text "project `pname' > " /// as err "Invalid list options = `bad'" exit 198 } preserve get_project_directory `pname' local pdir "`r(projectdir)'" local pfiles "`r(pfiles)'" local plinks "`r(plinks)'" local plog "`r(plog)'" dis _n(2) as txt "Project directory : " as res "`pdir'" _n // Check the project's files and links databases cap noi check_project_files_links `pname' "`pfiles'" "`plinks'" if _rc { dis as err "Problem with `pname'_files.dta or `pname'_links.dta;" /// " build project again" exit 459 } // Display the previous build status qui use "`pdir'/`pname'_links.dta", clear dis as text "Previous build start: " /// as res "`: char _dta[start_date]', `: char _dta[start_time]'" dis as text "Previous build end : " _cont if "`: char _dta[end_date]'" == "" { dis as err "unsuccessful" as text "" _n } else { dis as res "`: char _dta[end_date]', `: char _dta[end_time]'" _n } // log type is based on overall project setting and local option local logext = cond("`plog'" == "SMCL","smcl","log") if "`textlog'" != "" local logext "log" if "`smcllog'" != "" local logext "smcl" local listdir "`pdir'/archive/list" foreach w in `list' { // Make sure that the directory exist capture mkdir "`pdir'/archive" capture mkdir "`listdir'" // include date and time stamp in log file name local cdate "`c(current_date)'" local ctime "`c(current_time)'" local d : dis %dCYND date("`cdate'","`=cond(c(version) < 10,"dmy","DMY")'") local t = subinstr("`ctime'",":","",.) local logfile "`w'_`d'_`t'.`logext'" log using "`listdir'/`logfile'", name(list_log) // capture in case of user break cap noi project_list_`w' `pname', pdir("`pdir'") local rc = _rc log close list_log if `rc' exit `rc' } end program define project_list_build /* -------------------------------------------------------------------------------- List project files, according to the order they appear in the build. -------------------------------------------------------------------------------- */ syntax name(name=pname id="Project Name"), pdir(string) local pfiles "`pdir'/`pname'_files.dta" local plinks "`pdir'/`pname'_links.dta" // Identify project do-files qui use "`plinks'", clear qui by fdo: keep if _n == 1 rename fdo fno keep fno sort fno tempfile dofiles qui save "`dofiles'" // The master do-file is linked to all the files in the project. Use its // links to map-out the build, in the order they added. qui use "`plinks'", clear local status : char _dta[end_date] qui keep if fdo == 1 keep flink linktype norder level rename flink fno sort fno qui merge fno using "`pfiles'" qui keep if _merge == 3 drop _merge // Flag do-files sort fno qui merge fno using "`dofiles'" gen dofile = _merge == 3 qui drop if _merge == 2 // do-file not included in most recent build drop _merge // Indent filename by do-file levels sum level, meanonly local n = r(max) local sindent ". " forvalues i = 2/`n' { qui replace fname = "`sindent'" + fname if level == `i' local sindent "`sindent'. " } // Setup the table local tablevars fname flen linktype project_table_setup `tablevars', title1("Build Sequence (`c(N)' links)") local tw : char _dta[tablewidth] // Show files in the order processed in the build sort norder // Choose variable styles char fname[style] res char linktype[style] res forvalues i = 1/`c(N)' { if dofile[`i'] dis "{c |}{space `tw'}{c |}" project_table_line `tablevars', line(`i') if level[`i'] > level[`i'+1] dis "{c |}{space `tw'}{c |}" } // Add a warning if the previous build was unsuccessful if "`status'" == "" { dis "{c |}{space `tw'}{c |}" dis "{c |}" as err %~`tw's ">>> Incomplete Build <<<" as text "{c |}" } dis "{c |}{space `tw'}{c |}" dis "{c BLC}{hline `tw'}{c BRC}" end program define project_list_type /* -------------------------------------------------------------------------------- List project files, according to the type of file. These are do-files, log files, files linked as originals, files that are relied_on, and files created. -------------------------------------------------------------------------------- */ syntax name(name=pname id="Project Name"), pdir(string) local pfiles "`pdir'/`pname'_files.dta" local plinks "`pdir'/`pname'_links.dta" // Identify project do-files qui use "`plinks'", clear qui by fdo: keep if _n == 1 rename fdo fno keep fno sort fno tempfile dofiles qui save "`dofiles'" // The master do-file is linked to all the files in the project. Reduce // to one record per file. Files that are created and then used will // be represented by the "creates" record. qui use "`plinks'", clear local status : char _dta[end_date] qui keep if fdo == 1 keep flink linktype norder level sort flink norder qui by flink: keep if _n == 1 rename flink fno sort fno qui merge fno using "`pfiles'" qui keep if _merge == 3 drop _merge // Flag do-files sort fno qui merge fno using "`dofiles'" qui drop if _merge == 2 // do-file not included in most recent build qui gen ftype = 1 if _merge == 3 drop _merge // Flag log-files qui replace ftype = 2 if (linktype == 4) & /// (regexm(fname,".smcl$") | regexm(fname,".log$")) // Flag other types; type "uses" is always trumped by "creates" qui replace ftype = 3 if mi(ftype) & linktype == 1 qui replace ftype = 4 if linktype == 3 qui replace ftype = 5 if mi(ftype) & linktype == 4 local title1 Do-Files local title2 Log Files local title3 Original Files used (except do-files) local title4 Original Files that are relied upon local title5 Files created (except log files) tempfile f qui save "`f'" forvalues nt = 1/5 { use "`f'", clear // Setup the table local tablevars fname flen csum chngdate chngtime qui count if ftype == `nt' project_table_setup `tablevars', title1("`title`nt'' (`r(N)' files)") // Show files in alphabetical order qui gen Ufname = upper(fname) qui gen Ufpath = upper(fpath) sort Ufname Ufpath // Choose variable styles char fname[style] res qui keep if ftype == `nt' forvalues i = 1/`c(N)' { project_table_line `tablevars', line(`i') } // Add a warning if the previous build was unsuccessful if "`status'" == "" { local tw : char _dta[tablewidth] dis "{c |}{space `tw'}{c |}" dis "{c |}" as err %~`tw's ">>> Incomplete Build <<<" as text "{c |}" } dis "{c BLC}{hline `: char _dta[tablewidth]'}{c BRC}" } end program define project_list_index /* -------------------------------------------------------------------------------- List project files alphabetically -------------------------------------------------------------------------------- */ syntax name(name=pname id="Project Name"), pdir(string) local pfiles "`pdir'/`pname'_files.dta" local plinks "`pdir'/`pname'_links.dta" // The master do-file is linked to all the files in the project. Use it to // identify files in the project. qui use "`plinks'", clear local status : char _dta[end_date] qui keep if fdo == 1 keep flink sort flink qui by flink: keep if _n == 1 rename flink fno sort fno qui merge fno using "`pfiles'" qui keep if _merge == 3 drop _merge // Setup the table local tablevars fname flen csum chngdate chngtime project_table_setup `tablevars', title1("Alphabetical Index (`c(N)' files)") // Show files in alphabetical order qui gen Ufname = upper(fname) qui gen Ufpath = upper(fpath) sort Ufname Ufpath // Choose variable styles char fname[style] res forvalues i = 1/`c(N)' { project_table_line `tablevars', line(`i') } // Add a warning if the previous build was unsuccessful if "`status'" == "" { local tw : char _dta[tablewidth] dis "{c |}{space `tw'}{c |}" dis "{c |}" as err %~`tw's ">>> Incomplete Build <<<" as text "{c |}" } dis "{c BLC}{hline `: char _dta[tablewidth]'}{c BRC}" end program define project_list_directory /* -------------------------------------------------------------------------------- List project files by directory -------------------------------------------------------------------------------- */ syntax name(name=pname id="Project Name"), pdir(string) local pfiles "`pdir'/`pname'_files.dta" local plinks "`pdir'/`pname'_links.dta" // The master do-file is linked to all the files in the project. Reduce // to one record per file. Files that are created and then used will // be represented by the "creates" record. qui use "`plinks'", clear local status : char _dta[end_date] qui keep if fdo == 1 keep flink linktype norder level sort flink norder qui by flink: keep if _n == 1 rename flink fno sort fno qui merge fno using "`pfiles'" qui keep if _merge == 3 drop _merge // Setup the table local tablevars fname flen csum chngdate chngtime project_table_setup `tablevars', /// title1(" Alphabetical Index by Directory (`c(N)' files)") local tw : char _dta[tablewidth] // Show files in alphabetical order qui gen Ufname = upper(fname) qui gen Ufpath = upper(fpath) sort Ufpath Ufname // Choose variable styles char fname[style] res forvalues i = 1/`c(N)' { project_table_line `tablevars', line(`i') if fpath[`i'] != fpath[`i'+1] & `i' != 1 & `i' != `c(N)' /// dis "{c LT}{hline `tw'}{c RT}" } // Add a warning if the previous build was unsuccessful if "`status'" == "" { local tw : char _dta[tablewidth] dis "{c |}{space `tw'}{c |}" dis "{c |}" as err %~`tw's ">>> Incomplete Build <<<" as text "{c |}" } dis "{c BLC}{hline `: char _dta[tablewidth]'}{c BRC}" end program define project_list_concordance /* -------------------------------------------------------------------------------- List project files alphabetically with a list of all do-files that link to them. -------------------------------------------------------------------------------- */ syntax name(name=pname id="Project Name"), pdir(string) local pfiles "`pdir'/`pname'_files.dta" local plinks "`pdir'/`pname'_links.dta" // The master do-file is linked to all files in the previous build qui use "`plinks'", clear local status : char _dta[end_date] qui keep if fdo == 1 sort flink norder qui by flink: keep if _n == 1 // Ignore old links, i.e. links from do-files that did not run in the most // recent build keep flink rename flink fdo qui merge fdo using "`plinks'" qui keep if _merge == 3 // Reduce to link that originated in the do-filem i.e. ignore links // copies made for the enclosing do-file(s) qui keep if fdo == odo // Keep only one instance of each linktype sort fdo flink linktype norder qui by fdo flink linktype: keep if _n == 1 // The link to the do-file itself is obvious so we'll skip it. qui drop if fdo == flink // This is the main sample of what we want to list. tempfile fmain qui save "`fmain'" // Get file info for each link in the main sample. Reduce to one record // per linked file. These are the linked files that we want to list. keep flink sort flink qui by flink: keep if _n == 1 rename flink fno sort fno qui merge fno using "`pfiles'" qui keep if _merge == 3 drop _merge rename fno flink tempfile lkname qui save "`lkname'" // Get file info on the do-files for the list of linked files in the // main sample. use "`fmain'" keep fdo flink linktype rename fdo fno sort fno qui merge fno using "`pfiles'" qui keep if _merge == 3 drop _merge // combine with the list of unique linked files rename fno fdo qui append using "`lkname'" // Sort records by linked file. Put the record that contains the linked // file's name first and then the records from do-files that originated // the link. Put "uses" link after the "creates" link. gen dofile = fdo != . gen uses = linktype == 2 sort flink dofile uses fname fpath by flink: gen upfname = upper(fname[1]) qui by flink: gen upfpath = upper(fpath[1]) sort upfname upfpath dofile uses fname fpath // Indent the do-file name qui replace fname = "--> " + fname if dofile // Setup the table local tablevars fname flen csum linktype chngdate chngtime project_table_setup `tablevars', /// title1("Linked File to Do-file Concordance Table") local tw : char _dta[tablewidth] // Choose variable styles char fname[style] res forvalues i = 1/`c(N)' { if dofile[`i'] char fname[style] txt else char fname[style] res project_table_line `tablevars', line(`i') if flink[`i'] != flink[`i'+1] & `i' != 1 & `i' != `c(N)' /// dis "{c LT}{hline `tw'}{c RT}" } // Add a warning if the previous build was unsuccessful if "`status'" == "" { local tw : char _dta[tablewidth] dis "{c |}{space `tw'}{c |}" dis "{c |}" as err %~`tw's ">>> Incomplete Build <<<" as text "{c |}" } dis "{c BLC}{hline `: char _dta[tablewidth]'}{c BRC}" end program define project_list_archive /* -------------------------------------------------------------------------------- List the files that would be archived. Files to be archived are tracked using an archive flag maintained in the project files dataset. This allows each file to be tracked, irrespective of imcomplete builds or files from do-files that are moved in and out of the project. -------------------------------------------------------------------------------- */ syntax name(name=pname id="Project Name"), pdir(string) local pfiles "`pdir'/`pname'_files.dta" local plinks "`pdir'/`pname'_links.dta" // The master do-file is linked to all files in the previous build qui use "`plinks'", clear local status : char _dta[end_date] qui keep if fdo == 1 sort flink norder qui by flink: keep if _n == 1 // the archive task does not archive files that are created by the // project as these can be easily recreated by rebuilding it. qui drop if linktype == 4 keep flink rename flink fno qui merge fno using "`pfiles'" qui keep if _merge == 3 if "`: char _dta[archive_date]'" == "" { local title "No previous archive" } else { local title : dis "Status since Archive of " /// trim("`: char _dta[archive_date]'") " `: char _dta[archive_time]'" } // Flag files that have changed since the last archive qui gen status = cond(archiveflag,"chng","ok") char status[tname] Status // Setup the table local tablevars fname flen csum status chngdate chngtime project_table_setup `tablevars', title1("`title' (`c(N)' files)") // Show files in order they would be archived qui gen Ufname = upper(fname) qui gen Ufpath = upper(fpath) sort Ufpath Ufname // Choose variable styles char fname[style] res forvalues i = 1/`c(N)' { if status[`i'] == "ok" char status[style] res else char status[style] err project_table_line `tablevars', line(`i') } // Add a warning if the previous build was unsuccessful if "`status'" == "" { local tw : char _dta[tablewidth] dis "{c |}{space `tw'}{c |}" dis "{c |}" as err %~`tw's ">>> Incomplete Build <<<" as text "{c |}" } dis "{c BLC}{hline `: char _dta[tablewidth]'}{c BRC}" end program define project_list_cleanup /* -------------------------------------------------------------------------------- List files that are not part of the project (no link to them in the most recent build) that would be moved to an archive directory. -------------------------------------------------------------------------------- */ syntax name(name=pname id="Project Name"), pdir(string) local pfiles "`pdir'/`pname'_files.dta" local plinks "`pdir'/`pname'_links.dta" // Display the previous build status qui use "`plinks'", clear if "`: char _dta[end_date]'" == "" { dis as err "Previous build did not terminate normally" exit 459 } // Use the master do-file's links to identify files in the project qui keep if fdo == 1 keep flink sort flink qui by flink: keep if _n == 1 rename flink fno sort fno qui merge fno using "`pfiles'" qui keep if _merge == 3 drop _merge // add the project files and links databases to the list local nobs = _N + 1 qui set obs `nobs' qui replace fname = "`pname'_files.dta" in `nobs' local nobs = _N + 1 qui set obs `nobs' qui replace fname = "`pname'_links.dta" in `nobs' dis as txt _n "List of unused files that would be archived..." _n // recursively clean-up the project directory and subdirectories project_cleanupdir , currentdir(".") pdir("`pdir'") bdir("irrelevant") list dis end program define project_table_setup /* -------------------------------------------------------------------------------- This program converts numeric variables to be listed to string, calculates line lengths. This is a bit tricky because we can have very long file paths that will have to be wrapped around. -------------------------------------------------------------------------------- */ syntax varlist, title1(string) [title2(string)] // Convert numeric variables to string local numvars flen chngdate csum linktype local vlist : list numvars & varlist foreach v in `vlist' { `: char `v'[numconv]' rename `v' `v'0 rename s`v' `v' char rename `v'0 `v' } // Find the width of the table. Account for the width of each column title. local tw 0 foreach v of varlist `varlist' fpath { local tname : char `v'[tname] local minlen : length local tname gen n = length(`v') sum n, meanonly local w = cond(r(max) < `minlen', `minlen',r(max)) local tw = `tw' + `w' + 2 char `v'[width] `w' char `v'[style] txt drop n } // Allow fpath to wrap around if the table would be wider than c(linesize). // Do not allow the width of fpath to go below 20. local cw : char fpath[width] local tw = `tw' - `cw' local n = `c(linesize)' - (`tw' + 2) local cw = min(cond(`n' < 20, 20,`n'),`cw') char fpath[width] `cw' local tw = `tw' + `cw' local nspace = `tw' - `cw' - 1 char _dta[tablewidth] `tw' char _dta[tablespace] `nspace' char fname[align] - char fpath[align] - dis "{c TLC}{hline `tw'}{c TRC}" dis "{c |}" as res %~`tw's `"`title1'"' /// as text "{c |}" if `"`title2'"' != "" dis "{c |}" as res %~`tw's `"`title2'"' /// as text "{c |}" dis "{c |}{space `tw'}{c |}" dis "{c |}" _cont foreach v of varlist `varlist' fpath { dis as txt " " %`: char `v'[align]'`: char `v'[width]'s /// "`: char `v'[tname]'" " " _cont } dis "{c |}" dis "{c LT}{hline `tw'}{c RT}" end program define project_table_line /* -------------------------------------------------------------------------------- This program prints one line in the results, wrapping the file path as needed. -------------------------------------------------------------------------------- */ syntax varlist, line(integer) // Recover the widths we need to wrap around file paths local wfp : char fpath[width] local nspace : char _dta[tablespace] // Display all variables and the first line of the file path dis "{c |}" _cont foreach v of varlist `varlist' { dis " " as `: char `v'[style]' /// %`: char `v'[align]'`: char `v'[width]'s `v'[`line'] " " _cont } dis " " as `: char fpath[style]' %-`wfp's /// substr(fpath[`line'],1,`wfp') " {c |}" // generate extra lines until all of the file path is displayed local slen = length(fpath[`line']) local n `wfp' while `n' < `slen' { dis "{c |}{space `nspace'}" /// as `: char fpath[style]' %-`wfp's /// substr(fpath[`line'],`n'+1,`wfp') /// " {c |}" local n = `n' + `wfp' } end program define project_clear_globals /* -------------------------------------------------------------------------------- Drop all global macros without loosing local macros -------------------------------------------------------------------------------- */ macro drop _all end program define project_clear_dta_char /* -------------------------------------------------------------------------------- Drop all dataset-level char. This is used to avoid mixing data stored using char in the project files and links datasets. -------------------------------------------------------------------------------- */ local dtachar : char _dta[] foreach c of local dtachar { char _dta[`c'] } end