*! editanything v2.0.0 26may2026 *! Eric A. Booth, Sr Researcher, Texas 2036 (eric.a.booth@gmail.com) *! Elizabeth Teas, Sr Research Scientist, Far Harbor, LLC (elizabeth@farharbor.com) *! Open *any* text file (.ado, .sthlp, .hlp, .do, .mata, .md, .txt, .html, *! .raw, .csv, .tsv, .R, .py, .json, .yaml, .toml, .xml, .css, .js, .sql, *! .sh, .bat, .ini, .cfg, .log, .smcl, .tex, .bib, ...) from inside Stata. *! *! Default: opens in the Do-file Editor (like SSC's -adoedit-) but works on *! any text-readable extension. Options let you preview in the Viewer, hand *! the file to your OS default app, copy contents to the clipboard, create *! a new empty file, or safely clone a base/updates ado into PERSONAL. program define editanything, rclass version 14 syntax anything(name=fname id="filename" everything) [, /// EXTension(string) /// EDitor /// View /// EXTernal /// SHOWpath /// CLIPboard /// PERSONAL /// REPlace /// Force /// NEW /// Quietly /// ] *------------------------------------------------------------------* * 1. Clean the filename token *------------------------------------------------------------------* local fname : list clean fname local fname = subinstr(`"`fname'"', `"""', "", .) if `"`fname'"' == "" { di as err "filename required" exit 198 } * mutually-exclusive open modes local nmodes = 0 foreach m in editor view external showpath clipboard { if "``m''" != "" local ++nmodes } if `nmodes' > 1 { di as err "only one of {opt editor}, {opt view}, {opt external}, " /// "{opt showpath}, {opt clipboard} may be specified" exit 198 } * default extension to try when none supplied if "`extension'" == "" local extension "ado" local extension = subinstr("`extension'",".","",.) * does the filename already carry an extension? local has_ext = 0 local ext_given "" if regexm(`"`fname'"', "\.([A-Za-z0-9]+)$") { local has_ext = 1 local ext_given = strlower(regexs(1)) } *------------------------------------------------------------------* * 2. -new-: create a brand-new empty file, then open it *------------------------------------------------------------------* if "`new'" != "" { local target `"`fname'"' if !`has_ext' local target `"`target'.`extension'"' * make absolute if a bare name was given if !regexm(`"`target'"', "^(/|~|[A-Za-z]:)") { local target `"`c(pwd)'/`target'"' } capture confirm file `"`target'"' if !_rc { di as err `"file already exists: `target'"' di as err "omit {opt new} to open it, or pick a different name." exit 602 } tempname fh file open `fh' using `"`target'"', write text file close `fh' di as txt `"created new file: {browse "`target'":`target'}"' local file `"`target'"' } *------------------------------------------------------------------* * 3. Resolve an existing file by searching cwd, ado-path, PERSONAL *------------------------------------------------------------------* if "`new'" == "" { local file "" * if -personal- requested, look there first if "`personal'" != "" { local p1 `"`c(sysdir_personal)'`fname'"' capture confirm file `"`p1'"' if !_rc local file `"`p1'"' if "`file'" == "" & !`has_ext' { local p2 `"`c(sysdir_personal)'`fname'.`extension'"' capture confirm file `"`p2'"' if !_rc local file `"`p2'"' } } * direct / relative path as given if "`file'" == "" { capture confirm file `"`fname'"' if !_rc local file `"`fname'"' } * ado-path search exactly as typed if "`file'" == "" { capture findfile `"`fname'"' if !_rc local file `"`r(fn)'"' } * ado-path search with default extension appended if "`file'" == "" & !`has_ext' { capture findfile `"`fname'.`extension'"' if !_rc local file `"`r(fn)'"' } * if default ext was .ado and that failed, try .sthlp / .hlp / .do if "`file'" == "" & !`has_ext' & "`extension'" == "ado" { foreach try in sthlp hlp do { capture findfile `"`fname'.`try'"' if !_rc { local file `"`r(fn)'"' continue, break } } } * fall back to PERSONAL even if not requested if "`file'" == "" { local p1 `"`c(sysdir_personal)'`fname'"' capture confirm file `"`p1'"' if !_rc local file `"`p1'"' } if "`file'" == "" { di as err `"file not found: {bf:`fname'}"' di as err "searched: current directory, full ado-path, and PERSONAL" di as err "(tip: pass {opt new} to create a new empty file)" exit 601 } } *------------------------------------------------------------------* * 4. Normalize path: backslash -> /, expand leading ~ *------------------------------------------------------------------* local file : subinstr local file "\" "/", all local home : environment HOME if substr(`"`file'"',1,1) == "~" & !missing(`"`home'"') { local file : subinstr local file "~" "`home'" } *------------------------------------------------------------------* * 5. Capture metadata: extension, size, base-file flag *------------------------------------------------------------------* local fext "" if regexm(`"`file'"', "\.([A-Za-z0-9]+)$") local fext = strlower(regexs(1)) local fsize = . tempname fh capture file open `fh' using `"`file'"', read binary if !_rc { capture file seek `fh' eof if !_rc { file seek `fh' query local fsize = r(loc) } capture file close `fh' } local fbasename = substr(`"`file'"', strrpos(`"`file'"', "/")+1, .) local is_basefile = 0 if index(`"`file'"',"/ado/base") | index(`"`file'"',"/ado/updates") { local is_basefile = 1 } *------------------------------------------------------------------* * 6. -replace-: clone a base/updates file into PERSONAL, then edit *------------------------------------------------------------------* if "`replace'" != "" { if !`is_basefile' & "`personal'" == "" { di as txt "{opt replace} only relocates base/updates files; nothing to do." } else { local dest `"`c(sysdir_personal)'`fbasename'"' capture confirm file `"`dest'"' if !_rc { di as err `"PERSONAL copy already exists: `dest'"' di as err "delete it first, or open it directly:" di as err `" . editanything `fbasename', personal"' exit 602 } capture mkdir `"`c(sysdir_personal)'"' copy `"`file'"' `"`dest'"' di as txt `"copied: {input:`file'}"' di as txt `" to: {input:`dest'}"' local file `"`dest'"' local is_basefile = 0 } } *------------------------------------------------------------------* * 7. Pretty banner (unless -quietly-) *------------------------------------------------------------------* if "`quietly'" == "" { di as txt _n "{hline 70}" di as txt `" file: {browse "`file'":`file'}"' di as txt " type: ." as res "`fext'" as txt " " _c if !missing(`fsize') { di as txt "size: " as res %12.0fc `fsize' as txt " bytes" _c } if `is_basefile' di as txt " " as err "[Stata-supplied]" else di "" di as txt "{hline 70}" } return local file `"`file'"' return local extension `"`fext'"' return local basename `"`fbasename'"' if !missing(`fsize') return scalar size = `fsize' *------------------------------------------------------------------* * 8. Warn before editing Stata-supplied files *------------------------------------------------------------------* if `is_basefile' & "`force'" == "" & "`view'" == "" & "`showpath'" == "" /// & "`external'" == "" & "`clipboard'" == "" { di as err _n "Refusing to edit a Stata-supplied file in place." di as txt `" . editanything `fbasename', replace "' /// `"(copy to PERSONAL, then edit)"' di as txt `" . editanything `fbasename', view "' /// `"(open read-only in the Viewer)"' di as txt `" . editanything `fbasename', force "' /// `"(edit anyway; updates may overwrite)"' exit 0 } *------------------------------------------------------------------* * 9. Dispatch by mode *------------------------------------------------------------------* * -- showpath: already printed the clickable link if "`showpath'" != "" exit 0 * detect platform robustly (c(os) is "Unix" on modern macOS Stata) local _ismac = ("`c(os)'" == "MacOSX") | regexm("`c(machine_type)'", "Mac") local _iswindows = ("`c(os)'" == "Windows") * -- clipboard: copy raw file contents to system clipboard if "`clipboard'" != "" { if `_ismac' { shell cat "`file'" | pbcopy } else if `_iswindows' { shell type "`file'" | clip } else { shell cat "`file'" | xclip -selection clipboard } di as txt "contents copied to system clipboard." exit 0 } * -- external: hand off to the OS default application if "`external'" != "" { if `_ismac' { winexec open `"`file'"' } else if `_iswindows' { winexec cmd /c start "" `"`file'"' } else { winexec xdg-open `"`file'"' } di as txt "opened in OS default application." exit 0 } * -- view: read-only in Viewer (renders SMCL for .sthlp/.hlp/.smcl) if "`view'" != "" { local smclexts sthlp hlp smcl if `: list fext in smclexts' { view `"`file'"' } else { view `"`file'"', asis } exit 0 } *------------------------------------------------------------------* * 10. Default: open in the Do-file Editor *------------------------------------------------------------------* if "`force'" == "" & !missing(`fsize') { if `fsize' > 32000 & `c(stata_version)' < 11 { di as err _n "file is `fsize' bytes; Stata < 11 cannot doedit > 32 KB." di as txt "try: " as cmd `"editanything `fbasename', external"' _n /// " or " as cmd `"editanything `fbasename', view"' exit 603 } } capture noisily doedit `"`file'"' if _rc { di as err _n "doedit failed (rc=`_rc')." di as txt "alternatives:" di as txt `" . editanything `fbasename', view"' di as txt `" . editanything `fbasename', external"' exit _rc } end