--- /dev/null
+added to test mods to lifelog.hopto.org
sudo service postgresql restart
-## Install required libpq-dev to compile test perl driver, later.
+## Install required libpq-dev to compile test perl driver, later.
sudo apt install libpq-dev
## Install perl driver.
# Restore with:
psql $DB_NAME < "~/dev/LifeLog/dbLifeLog/bck-pg-$DB_NAME.sql"
+or
+pg_restore -U lifelog -d lifelog < lifelog.sql
+20231102 - Note bin/psql can have different default port number from the one in server config,
+ it is a separate client utility to postgres, bad thing refusing you connections.
+ So -h and -p options might be needed if using host settings which are for TCP sockets
+ The local setting is for linux sockets in the pg_hba.config file.
###############################
Important pgAdmin4 Notes
###############################
-What pgAdmin4 and their documentation and installation doesn't describe,
+What pgAdmin4 and their documentation and installation doesn't describe,
that the default web server is python based. The web based package will install the apache web server.
You most likely don't want that. And the pgAdmin-desktop, you possibly don't need that.
(Can use any browser instead) :)
Enter the email address and password to use for the initial pgAdmin user account:
Email address: user@domain.com
-Password:
+Password:
Retype password:
Starting pgAdmin 4. Please navigate to http://127.0.0.1:5050 in your browser.
* Serving Flask app "pgadmin" (lazy loading)
--- /dev/null
+{
+ "rootTopicKey": "c787a1ac-6cf8-4a19-8533-2011cfd25526",
+ "editorRootTopicKey": "c787a1ac-6cf8-4a19-8533-2011cfd25526",
+ "focusKey": "8a10c048-9a7a-49f6-b241-96c8f659882a",
+ "extData": {},
+ "topics": [
+ {
+ "key": "c787a1ac-6cf8-4a19-8533-2011cfd25526",
+ "parentKey": null,
+ "subKeys": [
+ "8a10c048-9a7a-49f6-b241-96c8f659882a",
+ "b604e11b-8a64-4596-b7a1-6b964d96f9da"
+ ],
+ "collapse": false,
+ "style": null,
+ "blocks": [
+ {
+ "type": "CONTENT",
+ "data": "Sample"
+ }
+ ]
+ },
+ {
+ "key": "8a10c048-9a7a-49f6-b241-96c8f659882a",
+ "parentKey": "c787a1ac-6cf8-4a19-8533-2011cfd25526",
+ "subKeys": [],
+ "collapse": false,
+ "style": null,
+ "blocks": [
+ {
+ "type": "CONTENT",
+ "data": "Test Code"
+ }
+ ]
+ },
+ {
+ "key": "b604e11b-8a64-4596-b7a1-6b964d96f9da",
+ "parentKey": "c787a1ac-6cf8-4a19-8533-2011cfd25526",
+ "subKeys": [],
+ "collapse": false,
+ "style": null,
+ "blocks": [
+ {
+ "type": "CONTENT",
+ "data": ""
+ }
+ ]
+ }
+ ],
+ "config": {
+ "readOnly": false,
+ "allowUndo": true,
+ "layoutDir": 0,
+ "theme": {
+ "name": "default",
+ "randomColor": true,
+ "background": "rgb(57,60,65)",
+ "highlightColor": "#50C9CE",
+ "marginH": 60,
+ "marginV": 20,
+ "contentStyle": {
+ "lineHeight": "1.5"
+ },
+ "linkStyle": {
+ "lineRadius": 5,
+ "lineType": "curve",
+ "lineWidth": "3px"
+ },
+ "rootTopic": {
+ "contentStyle": {
+ "fontSize": "34px",
+ "borderRadius": "35px",
+ "padding": "16px 18px 16px 18px"
+ },
+ "subLinkStyle": {
+ "lineType": "curve",
+ "lineWidth": "3px",
+ "lineColor": "rgb(113, 203, 45)"
+ }
+ },
+ "primaryTopic": {
+ "contentStyle": {
+ "borderWidth": "1px",
+ "borderStyle": "solid",
+ "borderRadius": "20px",
+ "fontSize": "17px",
+ "padding": "10px 15px 10px 15px"
+ },
+ "subLinkStyle": {
+ "lineType": "curve",
+ "lineWidth": "3px",
+ "lineColor": "rgb(113, 203, 45)"
+ }
+ },
+ "normalTopic": {
+ "contentStyle": {
+ "border": "1px solid #e8eaec",
+ "borderRadius": "20px",
+ "fontSize": "17px",
+ "padding": "4px 10px"
+ },
+ "subLinkStyle": {
+ "lineType": "curve",
+ "lineWidth": "3px",
+ "lineColor": "white"
+ }
+ }
+ }
+ },
+ "formatVersion": null
+}
\ No newline at end of file
##
use CGI::Carp qw(fatalsToBrowser set_message);
-##
-# This is a entry point script (main).
-##
-use lib::relative "system/modules";
+use lib "/home/will/dev/LifeLog/htdocs/cgi-bin/system/modules";
+#use lib::relative "system/modules";
require CNFParser;
require CNFNode;
our $GLOB_HTML_SERVE = "'{}/*.cgi' '{}/*.htm' '{}/*.html' '{}/*.md' '{}/*.txt'";
our $script_path = $0; $script_path =~ s/\w+.cgi$//;
-use constant LOG_Settings =>q(
+use constant LOG_Settings => q(
<<@<%LOG>
file = web_server.log
# Should it mirror to console too?
console = 0
# Disable/enable output to file at all?
- enabled = 0
+ enabled = 1
# Tail size cut, set to 0 if no tail cutting is desired.
- tail = 1000
+ tail = 60
>>
);
chomp $error;
$cgi->render(text=>qq(<html><body><font style="color:crimson; font-weight:bold">You have unfortunately hit an cgi-bin::CNFHTMLServiceError</font>
<div class='content-debug_output'><pre style="background:transparent">$error</pre><br> </div>
- </body></html>
+ </body></html>
)
);
}
);
- exit CNFHTMLService($cgi);
-};
+ # my $p = $cgi->_body_params->{keyed};
+ # $p->{'service'} = ['contacts'];
+ # $p->{'action'} = ['form'];
+
+ ##
+ # This is a entry point script (main).
+ ##
+ exit CNFHTMLService($cgi);
+};
-sub CNFHTMLService {
- my ($cgi,$ptr) = (shift, undef);
- my $cnf = CNFParser -> new (undef,{ DO_ENABLED => 1, HAS_EXTENSIONS=>1, ANONS_ARE_PUBLIC => 1, CGI=>$cgi });
- $cnf->parse(undef,_getServiceScript($cgi));
- $ptr = $cnf->data();
+sub CNFHTMLService($cgi) {
+ my $cnf = CNFParser -> new (undef,{ DO_ENABLED => 1, HAS_EXTENSIONS=>1, ANONS_ARE_PUBLIC => 1, CGI=>$cgi });
+ $cnf -> parse(undef,_getServiceScript($cgi));
+ my $ptr = $cnf->data();
$ptr = $ptr->{'PAGE'};
#say $$ptr if $ptr;
$cgi -> render(text=>$$ptr);
if($service eq 'feeds'){
return _CNF_Script_For_Feeds();
}
+ elsif($service eq 'contacts'){
+ return _CNF_Script_For_Contacts();
+ }
}
+sub _CNF_Script_For_Contacts {
+LOG_Settings . <<__CNF_IS_COOL__;
+<<<INCLUDE htdocs/cgi-bin/contact.cnf>>>
+<<PROVIDE_CONTACT_FORM<PLUGIN>
+
+ package : ContactsPlugin
+ subroutine : process
+ // The property holding the responce for the service.
+ property : ECHASH_CONTACT_RESPONSE
+ // The following maps in the CNF script the table and data properties for transfer/synch to the store.
+ // This shows how CNF can get complicated but also convenient to bind things in its scripts.
+ table : ECHASH_HOLDERS
+ data : CONTACTS_FORM_DATA
+
+>>
+__CNF_IS_COOL__
+}
+
+
sub _CNF_Script_For_Feeds {
LOG_Settings . <<__CNF_IS_COOL__;
<<PROCESS_RSS_FEEDS<PLUGIN>
else {
print
qq(<div id="menu_page" title="To close this menu click on its heart, and wait.">
-<div class="hdr" style="marging=0;padding:0px;">
+<div class="hdr">
<a id="to_top" href="#top" title="Go to top of page."><span class="ui-icon ui-icon-arrowthick-1-n" style="float:none;"></span></a>
Config
<a id="to_bottom" href="#bottom" title="Go to bottom of page."><span class="ui-icon ui-icon-arrowthick-1-s" style="float:none;"></span></a>
--- /dev/null
+!CNF3.0
+
+<<@<%WEB_SERVICE_SETTINGS>
+ $LOG_PATH = ../../dbLifeLog/
+ user =admin
+ pass = admin
+ dbi_source = DBI:SQLite:
+ dbi_store= contacts.db
+>>
+
+<< CONTACTS <TABLE>
+ ID INTEGER NOT NULL UNIQUE,
+ ID_FOR INTEGER NOT NULL UNIQUE,
+ Date TEXT NOT NULL,
+ FullName TEXT NOT NULL,
+ Message TEXT NOT NULL,
+ Status INTEGER,
+ PRIMARY KEY("ID" AUTOINCREMENT)
+>>
+<< ECHASH_HOLDERS <TABLE>
+ ID INTEGER NOT NULL UNIQUE,
+ Alias INTEGER NOT NULL UNIQUE,
+ ECHSH CHARACTER(16) NOT NULL,
+ Name varchar(64),
+ Email TEXT NOT NULL,
+ Details TEXT, PRIMARY KEY("ID" AUTOINCREMENT)
+>>
+
+<<CONTACTS_FORM_DATA<DATA>
+ID`Alias`ECHSH`Name`Image`Email`Details~
+#`webadmin`990MWWLWM8C2MI8K`Will Budic`images/webadmin.png`webadmin@fake.demo-email.com`Administrive Technical Contact.`~
+#`admin`990MWWLWM8C2MI8K`Will Budic`images/webadmin.png`webadmin@fake.demo-email.com`Engeenering site Owner Contact`~
+#`secretary`2435GG2G87HJLKLA`Rachel Zegler`images/zagler.png`zaglerr@fake.demo-email.com`Marketing Officer`~
+>>
+
+<<1>>
+
--- /dev/null
+# Electronic Contact Hash Message Specifications
+
+ ECMessage is an alternative to E-Mail postboxes, utilizing a webservice to provide
+ fast and secure one way contact messages for a website. To the website owner ore admin.
+
+ With this system in place, website scanners or email ants have no way of accessing email.
+ And do not feed them these, leaving email's in webpages. As will eventually SPAM you,
+ and possibly sell your details on the dark web.
+
+## ECHMessage Provides
+
+ * No Email server/client requirements.
+ * Secure service to service channel, if required.
+ * Spam prevention mechanism and message size limitation.
+ * ECHMessage to real email proxy, if setup.
+ * One Message store for multiple aliases, for easier management.
+ * Cross internetwork messaging closed away from the cloud.
+ * Strict private and public info store and presentation.
+ * private is email, real, name and address.
+ * public is alias and the hash.
+ * Hash is not global or domain centralized at all.
+
<<@<%WEBAPP_SETTINGS>
$LOG_PATH = ../../dbLifeLog/
- //We are reading only the css property, old way is the following hash, preserved as reminder.
+ //TODO We are reading only the css property, old way is the following hash, preserved as reminder.
$THEME = css => wsrc/main.css, colBG => #c8fff8, colSHDW => #9baec8
>>
margin:5px;
background: rgba(0, 223, 246, 0.13);
}
-
+ .textual {
+ width: 40%;
+ }
+ .textual p::first-letter {
+ color: blueviolet;
+ initial-letter: 3 2;
+ padding-right: 5pt;
+ }
#content ul {
padding-left: 20px;
}
<a class="ui-button ui-corner-all ui-widget" href="index.cgi">Index</a><hr>
<a class="ui-button ui-corner-all ui-widget" href="main.cgi">Life Log</a><hr>
<a class="ui-button ui-corner-all ui-widget" onclick="return fetchFeeds()">RSS Feeds</a>
-
>#>
>div>
<div<
<a class="ui-button ui-corner-all ui-widget" href="index.cgi">Index</a><hr>
<a class="a_" onclick="return demoLogin()">Demo</a><hr>
<a class="a_" onclick="return fetchFeeds()">Feeds</a><hr>
+ <a class="a_" onclick="return contactForm()">Contact</a><hr>
</div>
<div class="rz login">
$frm
<button type="button" onclick="return setNow();">Now</button>
<button type="reset" onclick="setNow();resetDoc(); return true;">Reset</button>
- <span id="cat_desc" name="cat_desc">Please Wait...</span>
+ <span id="cat_desc" name="cat_desc"><br>Please Wait...<img src="images/Wedges-9.1s-64px.png"></span>
Category:
else{$sql = $s.'➔'.$`}
$sideMenu = qq(
<div id="menu_page" title="To close this menu click on its heart, and wait." style="visibility:hidden">
- <div class="hdr" style="marging=0;padding:0px;">
+ <div class="hdr">
<a id="to_bottom" href="#bottom" title="Go to bottom of page."><span class="ui-icon ui-icon-arrowthick-1-s" style="float:none;"></span></a>
<a id="dutch_left" onclick="return moveMenuLeft();"><span class="ui-icon ui-icon-arrowthick-1-w" style="float:none;"></span></a>
Life
--- /dev/null
+/home/will/LifeLog/htdocs/
\ No newline at end of file
}
sub _toCNFDate ($formated, $timezone) {
my $dt = DateTime::Format::DateParse->parse_datetime($formated, $timezone);
+ die "Unable to parse date:" if not $dt;
return new('CNFDateTime',{epoch => $dt->epoch, datetime=>$dt, TZ=>$timezone});
}
sub _listAvailableCountryCodes(){
use warnings;
###
-# TREE instuction meta.
-use constant HAS_PRIORITY => "HAS_PROCESSING_PRIORITY"; # Schedule to process before the rest in synchronous line of instructions.
-
-#
-###
-# DO instruction meta.
-#
-use constant ON_DEMAND => "ON_DEMAND"; #Postpone to evaluate on demand.
-use constant SHELL => "SHELL"; #Execute via system shell.
-
-#
-
-###
-# Returns the regular expresion for any of this meta constances.
+# Returns the regular expresion for any of the meta constances.
##
sub _meta {
my $constance = shift;
}
$constance;
}
+#
+
###
# Priority order no. for instructions.
use constant PRIORITY => qr/(\s*\_+PRIORITY\_(\d+)\_+\s*)/o;
-###
-# Tree instruction has been scripted in collapsed nodes shorthand format.
-# Shortife is parsed faster and with less recursion, but can be prone to script errors,
-# resulting in unintended placings.
-use constant IN_SHORTIFE => qr/(\s*\_+IN_SHORTIFE\_+\s*)/o;
-sub import {
+sub import {
my $caller = caller; no strict "refs";
{
- *{"${caller}::meta"} = \&_meta;
- *{"${caller}::meta_has_priority"} = sub {return _meta(HAS_PRIORITY)};
+
+ # TREE instuction meta.
+ *{"${caller}::meta_has_priority"} = sub {return _meta("HAS_PROCESSING_PRIORITY")};
+ # Schedule to process before the rest in synchronous line of instructions.
*{"${caller}::meta_priority"} = \&PRIORITY;
- *{"${caller}::meta_on_demand"} = sub {return _meta(ON_DEMAND)};
- *{"${caller}::meta_node_in_shortife"} =\&IN_SHORTIFE;
- *{"${caller}::SHELL"} = \&SHELL;
+ #Postpone to evaluate on demand.
+ *{"${caller}::meta_on_demand"} = sub {return _meta("ON_DEMAND")};
+ # Process or load last (includes0.
+ *{"${caller}::meta_process_last"} = sub {return _meta("PROCESS_LAST")};
+ ###
+ # Tree instruction has been scripted in collapsed nodes shorthand format.
+ # Shortife is parsed faster and with less recursion, but can be prone to script errors,
+ # resulting in unintended placings.
+ *{"${caller}::meta_node_in_shortife"} = sub {return _meta("IN_SHORTIFE")};
+ # Execute via system shell.
+ *{"${caller}::SHELL"} = sub {return _meta("SHELL")};
+ # Returns the regular expresion for any of the meta constances.
+ *{"${caller}::meta"} = \&_meta;
}
- return 1;
+ return 1;
+}
+###
+# CNF DATA instruction headers can contain extra expected data type meta info.
+# This will strip them out and build the best expected SQL create table body, based on this meta.
+# I know, this is crazy stuff, skips having to have to use the TABLE instruction in most cases.
+###
+sub _metaTranslateDataHeader {
+ my $isPostgreSQL = shift;
+ my @array = @_;
+ my ($idType,$body,$primary)=('NONE');
+ my ($INT,$BOOL,$TEXT,$DATE,$ID, $CNFID, $INDEX) = (
+ _meta('INT'),_meta('BOOL'),_meta('TEXT'),_meta('DATE'),
+ _meta('ID'),_meta('CNF_ID'),_meta('CNF_INDEX')
+ );
+ for my $i (0..$#array){
+ my $hdr = $array[$i];
+ if($hdr eq 'ID'){
+ if($isPostgreSQL){
+ $body .= "\"$hdr\" INT UNIQUE PRIMARY KEY GENERATED ALWAYS AS IDENTITY,\n";
+ #$primary = "PRIMARY KEY(ID)"
+ }else{
+ $body .= "\"$hdr\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n";
+ }
+ # DB provited sequence, you better don't set this when inserting a record.
+ $idType = 'AUTOINCREMENT'
+ }elsif($hdr =~ s/$CNFID/""/ei){
+ #This is where CNF provides the ID uinque int value (which doesn't have to be autonumbered i.e. '#', but must be unique).
+ $body .= "\"$hdr\" INTEGER NOT NULL PRIMARY KEY CHECK (\"$hdr\">0),\n";
+ $idType = 'CNF_INDEX'
+ }elsif($hdr =~ s/$ID/""/ei){
+ #This is ID prefix to some other data id stored in this table, usually one to one/many relationship.
+ $body .= "\"$hdr\" INTEGER CHECK (\"$hdr\">0),\n";
+ }elsif($hdr =~ s/$INDEX/""/ei){
+ # This is where CNF instructs to make a indexed lookup type field,
+ # for inside database fast selecting, hashing, caching and other queries.
+ $body .= "\"$hdr\" varchar(64) NOT NULL PRIMARY KEY,\n";
+ }elsif($hdr =~ s/$INT/""/ei){
+ $body .= "\"$hdr\" INTEGER NOT NULL,\n";
+ }elsif($hdr =~ s/$BOOL/''/ei){
+ if($isPostgreSQL){
+ $body .= "\"$hdr\" BOOLEAN NOT NULL,\n";
+ }else{
+ $body .= "\"$hdr\" BOOLEAN NOT NULL CHECK (\"$hdr\" IN (0, 1)),\n";
+ }
+ }elsif($hdr =~ s/$TEXT/""/ei){
+ $body .= "\"$hdr\" TEXT NOT NULL CHECK (length(\"$hdr\")<=2024),\n";
+ }elsif($hdr =~ s/$DATE/""/ei){
+ $body .= "\"$hdr\" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n";
+ }else{
+ $body .= "\"$hdr\" TEXT NOT NULL,\n";
+ }
+ $array[$i] = $hdr;
+ }
+ if($primary){
+ $body .= $primary;
+ }else{
+ $body =~ s/,$//
+ }
+return [\@array,\$body,$idType];
}
-
1;
\ No newline at end of file
use Syntax::Keyword::Try;
use Hash::Util qw(lock_hash unlock_hash);
use File::ReadBackwards;
+use File::Copy;
require CNFMeta; CNFMeta::import();
require CNFNode;
our %lists;
our %properties;
our %instructors;
+our $SQL;
###
# Package fields are always global in perl!
###
our %ANONS;
#private -> Instance fields:
- my $anons;
- my %includes;
+ my $anons;
+ my @includes; my $CUR_SCRIPT;
my %instructs;
+ my $IS_IN_INCLUDE_MODE;
+ my $LOG_TRIM_SUB;
###
# CNF Instruction tag covered reserved words.
# You can't use any of these as your own possible instruction implementation, unless in lower case.
$self->{STRICT} = 1 if not exists $self->{STRICT}; #make strict by default if missing.
$self->{ENABLE_WARNINGS} = 1 if not exists $self->{ENABLE_WARNINGS};
$self->{HAS_EXTENSIONS} = 0 if not exists $self->{HAS_EXTENSIONS};
- $self->{RUN_PROCESSORS} = 1 if not exists $self->{RUN_PROCESSORS}; #Bby default enabled, disable during script dev.
+ $self->{RUN_PROCESSORS} = 1 if not exists $self->{RUN_PROCESSORS}; #By default enabled, disable during script dev.
+ # Autoload the data type properties placed in a separate file, from a FILE instruction.
+ $self->{AUTOLOAD_DATA_FILES} =1 if not exists $self->{AUTOLOAD_DATA_FILES};
$self->{CNF_VERSION} = VERSION;
$self->{__DATA__} = {};
+ undef $SQL;
bless $self, $class; $self->parse($path, undef, $del_keys) if($path);
return $self;
}
our $meta_has_priority = meta_has_priority();
our $meta_priority = meta_priority();
our $meta_on_demand = meta_on_demand();
+our $meta_process_last = meta_process_last();
+
+
###
# The metaverse is that further this can be expanded,
# to provide further dynamic meta processing of the property value of an anon.
sub _isTrue{
my $value = shift;
return 0 if(not $value);
- return ($value =~ /1|true|yes|on/i)
+ return ($value =~ /1|true|yes|on|t|da/i)
}
###
# Post parsing instructed special item objects. They have lower priority to Order of apperance and from CNFNodes.
}
}
$ret = $args->process($n,$ret);
-
}elsif($ref eq 'HASHREF'){
foreach my $key(keys %$args){
if($ret =~ m/\$\$\$$key\$\$\$/g){
###
# Collection now returns the contained type dereferenced and is concidered a property.
# Make sure you use the appropriate Perl type on the receiving end.
-# Note, if properties contain any scalar key entry, it sure hasn't been set by this parser.
+# Note, if properties contain any scalar key row, it sure hasn't been set by this parser.
#
sub property { my($self, $name) = @_;
if(exists($properties{$name})){
#private to parser sub.
sub doInstruction { my ($self,$e,$t,$v) = @_;
-
my $DO_ENABLED = $self->{'DO_ENABLED'}; my $priority = 0;
$t = "" if not defined $t;
-
if($t eq 'CONST' or $t eq 'CONSTANT'){#Single constant with mulit-line value;
# It is NOT allowed to overwrite constant.
if (not $self->{$e}){
$anons->{$e} = $v;
}
elsif($t eq 'DATA'){
- $v=~ s/^\n//;
- foreach(split /~\n/,$v){
- my @a;
- $_ =~ s/\\`/\\f/g;#We escape to form feed the found 'escaped' backtick so can be used as text.
- foreach my $d (split /`/, $_){
- $d =~ s/\\f/`/g; #escape back form feed to backtick.
- $d =~ s/~$//; #strip dangling ~ if there was no \n
- $t = substr $d, 0, 1;
- if($t eq '$'){
- $v = $d; #capture specked value.
- $d =~ s/\$$|\s*$//g; #trim any space and system or constant '$' end marker.
- if($v=~m/\$$/){
- $v = $self->{$d}; $v="" if not $v;
- }
- else{
- $v = $d;
- }
- push @a, $v;
- }
- else{
- if($t =~ /^\#(.*)/) {#First is usually ID a number and also '#' signifies number.
- $d = $1;#substr $d, 1;
- $d=0 if !$d; #default to 0 if not specified.
- push @a, $d
- }
- else{
- push @a, $d;
- }
- }
- }
-
- my $existing = $self->{'__DATA__'}{$e};
- if(defined $existing){
- my @rows = @$existing;
- push @rows, [@a] if scalar @a >0;
- $self->{'__DATA__'}{$e} = \@rows
- }else{
- my @rows; push @rows, [@a];
- $self->{'__DATA__'}{$e} = \@rows if scalar @a >0;
- }
- }
-
+ $self->doDataInstruction_($e,$v)
}elsif($t eq 'DATE'){
if($v && $v !~ /now|today/i){
$v =~ s/^\s//;
$v = CNFDateTime::_toCNFDate($v,$self->{'TZ'});
}else{
- $v = CNFDateTime->new(TZ=>$self->{'TZ'});
+ $v = CNFDateTime->new({TZ=>$self->{'TZ'}});
}
$anons->{$e} = $v;
}elsif($t eq 'FILE'){#@TODO Test case this
- my ($i,$path,$cnf_file) = (0,"",$self->{CNF_CONTENT});
- $v=~s/\s+//g;
- $path = substr($path, 0, rindex($cnf_file,'/')) .'/'.$v;
- push @files, $path;
- next if !$self->{'$AUTOLOAD_DATA_FILES'};
- open(my $fh, "<:perlio", $path ) or CNFParserException->throw("Can't open $path -> $!");
- read $fh, my $content, -s $fh;
- close $fh;
- my @tags = ($content =~ m/<<(\w*<(.*?).*?>>)/gs);
- foreach my $tag (@tags){
- next if not $tag;
- my @kv = split /</,$tag;
- $e = $kv[0];
- $t = $kv[1];
- $i = index $t, "\n";
- if($i==-1){
- $t = $v = substr $t, 0, (rindex $t, ">>");
- }
- else{
- $v = substr $t, $i+1, (rindex $t, ">>")-($i+1);
- $t = substr $t, 0, $i;
- }
- if($t eq 'DATA'){
- foreach(split /~\n/,$v){
- my @a;
- $_ =~ s/\\`/\\f/g;#We escape to form feed the found 'escaped' backtick so can be used as text.
- foreach my $d (split(/`/, $_)){
- $d =~ s/\\f/`/g; #escape back form feed to backtick.
- $t = substr $d, 0, 1;
- if($t eq '$'){
- $v = $d; #capture spected value.
- $d =~ s/\$$|\s*$//g; #trim any space and system or constant '$' end marker.
- if($v=~m/\$$/){
- $v = $self->{$d}; $v="" if not $v;
- }
- else{
- $v = $d;
- }
- push @a, $v;
- }
- else{
- if($t =~ /^\#(.*)/) {#First is usually ID a number and also '#' signifies number.
- $d = $1;#substr $d, 1;
- $d=0 if !$d; #default to 0 if not specified.
- push @a, $d
- }
- else{
- push @a, $d;
- }
- }
- my $existing = $self->{'__DATA__'}{$e};
- if(defined $existing){
- my @rows = @$existing;
- push @rows, [@a] if scalar @a >0;
- $self->{'__DATA__'}{$e} = \@rows
- }else{
- my @rows; push @rows, [@a];
- $self->{'__DATA__'}{$e} = \@rows if scalar @a >0;
- }
- }
- }
- }
- }
+ $self->doLoadDataFile($e,$v);
}elsif($t eq 'INCLUDE'){
- $includes{$e} = {loaded=>0,path=>$e,v=>$v};
+ if (!$v){
+ $v=$e
+ }else{
+ $anons = $v;
+ }
+ my $prc_last = ($v =~ s/($meta_process_last)/""/ei)?1:0;
+ if (includeContains($v)){
+ $self->warn("Skipping adding include $e, path already is registered for inclusion -> $v");
+ return;
+ }
+ $includes[@includes] = {script=>$v,local=>$CUR_SCRIPT,loaded=>0, prc_last=>$prc_last};
}elsif($t eq 'TREE'){
my $tree = 0;
if (!$v){
$v = $e;
$e = 'LAST_DO';
}
- if( $v =~ s/($meta_has_priority)/""/ei){
+ if( $v =~ s/($meta_has_priority)/""/ei ){
$priority = 1;
}
- if( $v =~ s/$meta_priority/""/sexi){
+ if( $v =~ s/$meta_priority/""/sexi ){
$priority = $2;
}
$tree = CNFNode->new({'_'=>$e,'~'=>$v,'^'=>$priority});
$tree->{DEBUG} = 1 if $self->{DEBUG};
$instructs{$e} = $tree;
- }elsif($t eq 'TABLE'){ # This has now be late bound and send to the CNFSQL package. since v.2.6
- SQL()->createTable($e,$v) } # It is hardly been used. But in future this might change.
- elsif($t eq 'INDEX'){ SQL()->createIndex($v)}
+ }elsif($t eq 'TABLE'){ # This all have now be late bound and send via the CNFSQL package. since v.2.6
+ # It is hardly been used. But in the future this might change.
+ my $type = "NONE"; if ($v =~ 'AUTOINCREMENT'){$type = "AUTOINCREMENT"}
+ $self->SQL()->createTable($e,$v,$type) }
+ elsif($t eq 'INDEX'){ $self->SQL()->createIndex($v)}
elsif($t eq 'VIEW'){ SQL()->createView($e,$v)}
- elsif($t eq 'SQL'){ SQL($e,$v)}
- elsif($t eq 'MIGRATE'){SQL()->migrate($e, $v)
+ elsif($t eq 'SQL'){ $self->SQL($e,$v)}
+ elsif($t eq 'MIGRATE'){$self->SQL()->migrate($e, $v)
}
elsif($t eq 'DO'){
if($DO_ENABLED){
$v = $e;
$e = 'LAST_DO';
}
- if( $v =~ s/($meta_has_priority)/""/ei){
+ if( $v =~ s/($meta_has_priority)/""/ei ){
$priority = 1;
}
- if( $v =~ s/($meta_priority)/""/sexi){
+ if( $v =~ s/($meta_priority)/""/sexi ){
$priority = $2;
}
- if($v=~ s/($meta_on_demand)/""/ei){
+ if( $v=~ s/($meta_on_demand)/""/ei ){
$anons->{$e} = CNFNode -> new({'_'=>$e,'&'=>$v,'^'=>$priority});
return;
}
}
}
}
+sub doLoadDataFile { my ($self,$e,$v)=@_;
+ my ($path,$cnf_file) = ("",$self->{CNF_CONTENT});
+ $v=~s/\s+//g;
+ if(! -e $v){
+ $path = substr($path, 0, rindex($cnf_file,'/')) .'/'.$v;
+ }
+ foreach(@files){
+ return if $_ eq $path
+ }
+ return if not _isTrue($self->{AUTOLOAD_DATA_FILES});
+ #
+ $self->loadDataFile($e,$path)
+}
+sub loadDataFile { my ($self,$e,$path,$v,$i)=@_;
+
+ open(my $fh, "<:perlio", $path ) or CNFParserException->throw("Can't open $path -> $!");
+ read $fh, my $content, -s $fh;
+ close $fh;
+ #
+ push @files, $path;
+ my @tags = ($content =~ m/<<(\w*<(.*?).*?>>)/gs);
+ foreach my $tag (@tags){
+ next if not $tag;
+ my @kv = split /</,$tag;
+ $e = $kv[0];
+ $tag = $kv[1];
+ $i = index $tag, "\n";
+ if($i==-1){
+ $tag = $v = substr $tag, 0, (rindex $tag, ">>");
+ }
+ else{
+ $v = substr $tag, $i+1, (rindex $tag, ">>")-($i+1);
+ $tag = substr $tag, 0, $i;
+ }
+ if($tag eq 'DATA'){
+ $self->doDataInstruction_($e,$v)
+ }
+ }
+}
+#private
+sub doDataInstruction_{ my ($self,$e,$v,$t,$d)=@_;
+ my $add_as_SQLTable = $v =~ s/${meta('SQL_TABLE')}/""/sexi;
+ my $isPostgreSQL = $v =~ s/${meta('SQL_PostgreSQL')}/""/sexi;
+ my $isHeader = 0;
+ $v=~ s/^\s*//gm;
+ foreach my $row(split(/~\s/,$v)){
+ my @a;
+ $row =~ s/\\`/\\f/g;#We escape to form feed the found 'escaped' backtick so can be used as text.
+ my @cols = $row =~ m/([^`]*)`{0,1}/gm;pop @cols;#<-regexp is special must pop last empty element.
+ foreach my $d(@cols){
+ $d =~ s/\\f/`/g; #escape back form feed to backtick.
+ $d =~ s/^\s*|~$//g; #strip dangling ~ if there was no \n
+ $t = substr $d, 0, 1;
+ if($t eq '$'){
+ $v = $d; #capture specked value.
+ $d =~ s/\$$|\s*$//g; #trim any space and system or constant '$' end marker.
+ if($v=~m/\$$/){
+ $v = $self->{$d};
+ }
+ else{
+ $v = $d;
+ }
+ $v="" if not $v;
+ push @a, $v;
+ }
+ else{
+ if($d =~ /^\#(.*)/) {#First is usually ID a number and also '#' signifies number.
+ $d = $1;
+ $d=0 if !$d; #default to 0 if not specified.
+ push @a, $d
+ }
+ else{
+ $d="" if not $d;
+ push @a, $d;
+ }
+ }
+ }
+ if($add_as_SQLTable){
+ my ($INT,$BOOL,$TEXT,$DATE) = (meta('INT'),meta('BOOL'),meta('TEXT'),meta('DATE'));
+ my $ret = CNFMeta::_metaTranslateDataHeader($isPostgreSQL,@a);
+ my @hdr = @$ret;
+ @a = @{$hdr[0]};
+ $self->SQL()->createTable($e,${$hdr[1]},$hdr[2]);
+ $add_as_SQLTable = 0;$isHeader=1;
+ }
+
+ my $existing = $self->{'__DATA__'}{$e};
+ if(defined $existing){
+ if($isHeader){$isHeader=0;next}
+ my @rows = @$existing;
+ push @rows, [@a] if scalar @a >0;
+ $self->{'__DATA__'}{$e} = \@rows
+ }else{
+ my @rows; push @rows, [@a];
+ $self->{'__DATA__'}{$e} = \@rows if scalar @a >0;
+ }
+ }
+}
###
# Parses a CNF file or a text content if specified, for this configuration object.
}else{
$anons = $self->{'__ANONS__'};
}
- #private %includes; for now we keep on possible multiple calls to parse.
- #private instructs on this parse call.
- %instructs = ();
# We control from here the constances, as we need to unlock them if previous parse was run.
unlock_hash(%$self);
close $fh;
my @stat = stat($cnf_file);
$self->{CNF_STAT} = \@stat;
- $self->{CNF_CONTENT} = $cnf_file;
+ $self->{CNF_CONTENT} = $CUR_SCRIPT = $cnf_file;
}else{
- my $type =Scalar::Util::reftype($content);
+ my $type = Scalar::Util::reftype($content);
if($type && $type eq 'ARRAY'){
$content = join "",@$content;
$self->{CNF_CONTENT} = 'ARRAY';
- }else{$self->{CNF_CONTENT} = 'script'};
+ }else{
+ $CUR_SCRIPT = \$content;
+ $self->{CNF_CONTENT} = 'script'
+ }
}
$content =~ m/^\!(CNF\d+\.\d+)/;
my $CNF_VER = $1; $CNF_VER="Undefined!" if not $CNF_VER;
$line =~ s/\s*>$//;
$line =~ m/([\$\w]*)(\s*=\s*)(.*)/g;
my $name = $1;
- $line = $3; $line =~ s/^\s*(['"])(.*)\g{1}$/$2/;#strip quotes
+ $line = $3; $line =~ s/^\s*(['"])(.*)\g{1}$/$2/ if $line;#strip quotes
if(defined $name){
if($isVar){
$anons ->{$name} = $line if $line
}
}
}else{
- doInstruction($self,$v,$t,undef);
+ doInstruction($self,$v,$t,undef);
}
}else{
$v =~ s/\s*>$//;
doInstruction($self,$e,$t,$v)
}
}
+ # Do scripted includes first. As these might set properties imported and processed used by the main script.
+ if(@includes){
+ $includes[@includes] = {script=>$CUR_SCRIPT,loaded=>1, prc_last=>0} if not includeContains($CUR_SCRIPT); #<- to prevent circular includes.
+ foreach (@includes){
+ $self -> doInclude($_) if $_ && not $_->{prc_last} and not $_->{loaded} and $_->{local} eq $CUR_SCRIPT;
+ }
+ }
### Do the smart instructions and property linking.
- if(%instructs){
+ if(%instructs && not $IS_IN_INCLUDE_MODE){
my @items;
foreach my $e(keys %instructs){
my $struct = $instructs{$e};
}
undef %instructs;
}
- #Do scripted includes.
- my @inc = sort values %includes;
- $includes{$0} = {loaded=>1, path=>$self->{CNF_CONTENT}}; #<- to prevent circular includes.
- foreach my $file(@inc){
- if(!$file->{loaded} && $file->{path} ne $self->{CNF_CONTENT}){
- if(open(my $fh, "<:perlio", $file->{path} )){
- read $fh, $content, -s $fh;
- close $fh;
- if($content){
- $file->{loaded} = 1;
- $self->parse(undef, $content)
- }else{
- $self->error("Include content is blank for -> ".$file->{path})
- }
- }else{
- CNFParserException->throw("Can't open ".$file->{path}." -> $!") if $self->{STRICT};
- $file->{loaded} = 0;
- $self->error("Script include not available -> ".$file->{path})
- }
- }
+
+ foreach (@includes){
+ $self -> doInclude($_) if $_ && (not $_->{loaded} and $_->{local} eq $CUR_SCRIPT)
}
+ undef @includes if not $IS_IN_INCLUDE_MODE;
+
foreach my $k(@$del_keys){
delete $self->{$k} if exists $self->{$k}
}
my $runProcessors = $self->{RUN_PROCESSORS} ? 1: 0;
lock_hash(%$self);#Make repository finally immutable.
runPostParseProcessors($self) if $runProcessors;
+ if ($LOG_TRIM_SUB){
+ $LOG_TRIM_SUB->();
+ undef $LOG_TRIM_SUB;
+ }
return $self
}
#
+ sub includeContains{
+ my $path = shift;
+ foreach(@includes){
+ return 1 if $_&&$_->{script} eq $path
+ }
+ return 0
+ }
+###
+# Loads and parses includes local to script.
+###
+sub doInclude { my ($self, $prp_file) = @_;
+ if(!$prp_file->{loaded}){
+ my $file = $prp_file->{script};
+ if(!-e $file){$file =~ m/.*\/(.*$)/; $file = $1}
+ if(open(my $fh, "<:perlio", $file)){
+ read $fh, my $content, -s $fh;
+ close $fh;
+ if($content){
+ my $cur_script = $CUR_SCRIPT;
+ $prp_file->{loaded} = 1;
+ $CUR_SCRIPT = $prp_file->{script};
+ # Perl is not OOP so instructions are gathered into one place, time will tell if this is desirable rather then a curse.
+ # As per file processing of instructions is not encapsulated within a included file, but main includer or startup script.
+ $IS_IN_INCLUDE_MODE = 1;
+ $self->parse(undef, $content);
+ $IS_IN_INCLUDE_MODE = 0;
+ $CUR_SCRIPT = $cur_script;
+ }else{
+ $self->error("Include content is blank for include -> ".$prp_file->{script})
+ }
+ }else{
+ $prp_file->{loaded} = 0;
+ $self->error("Script include not available for include -> ".$prp_file->{script});
+ CNFParserException->throw("Can't open include ".$prp_file->{script}." -> $!") if $self->{STRICT};
+ }
+ }
+}
sub instructPlugin {
my ($self, $struct, $anons) = @_;
open (my $fh, ">>", $logfile) or die $!;
print $fh $time . " - " . $message ."\n";
close $fh;
- if(_isTrue($log{tail}) && $tail_cnt){
- my $fh = File::ReadBackwards->new($logfile) or die $!;
- if($fh->{lines}>$tail_cnt){
- my $pos = do {
- $fh->readline() for 1..$tail_cnt;
- $fh->tell()
- };
- truncate($logfile, $pos) or die $!;
- }
+ if($tail_cnt>0 && !$LOG_TRIM_SUB){
+ $fh = File::ReadBackwards->new($logfile) or die $!;
+ if($fh->{lines}>$tail_cnt){
+ $LOG_TRIM_SUB = sub {
+ my $fh = File::ReadBackwards->new($logfile) or die $!;
+ my @buffer; $buffer[@buffer] = $fh->readline() for (1..$tail_cnt);
+ open (my $fhTemp, ">", "/tmp/$logfile") or die $!;
+ print $fhTemp $_ foreach (reverse @buffer);
+ close $fhTemp;
+ move("/tmp/$logfile",$logfile)
+ }
+ }
}
}
}
foreach (keys(%ENV)){print $_,"=", "\'".$ENV{$_}."\'", "\n"}
}
-our $SQL;
sub SQL {
- if(!$SQL){##It is late compiled on demand.
- require CNFSQL; $SQL = CNFSQL->new();
+ if(!$SQL){##It is late compiled package on demand.
+ my $self = shift;
+ my $data = shift;
+ require CNFSQL; $SQL = CNFSQL->new({parser=>$self});
}
$SQL->addStatement(@_) if @_;
return $SQL;
my $self = shift;
if(!$JSON){
require CNFJSON;
- $JSON = CNFJSON-> new({ CNF_VERSION=>$self->{CNF_VERSION},
- CNF_CONTENT=>$self->{CNF_CONTENT},
- DO_ENABLED =>$self->{DO_ENABLED}
+ $JSON = CNFJSON-> new({ CNF_VERSION => $self->{CNF_VERSION},
+ CNF_CONTENT => $self->{CNF_CONTENT},
+ DO_ENABLED => $self->{DO_ENABLED}
});
}
return $JSON;
}
sub END {
+$LOG_TRIM_SUB->() if $LOG_TRIM_SUB;
undef %ANONS;
undef @files;
undef %properties;
--- /dev/null
+###
+# SQL Processing part for the Configuration Network File Format.
+###
+package CNFSQL;
+
+use strict;use warnings;#use warnings::unused;
+use Exception::Class ('CNFSQLException'); use Carp qw(cluck);
+use Syntax::Keyword::Try;
+use Time::HiRes qw(time);
+use DateTime;
+use DBI;
+use Tie::IxHash;
+
+use constant VERSION => '2.0';
+
+our %tables = (); our %tables_id_type = ();
+our %views = ();
+our %mig = ();
+our @sql = ();
+our @statements;
+our %curr_tables = ();
+
+my $isPostgreSQL = 0;
+my $hasRecords = 0;
+my $TZ;
+
+
+sub new {
+ my ($class, $attrs, $self) = @_;
+ $self = \%$attrs;
+ # By convention any tables and views as appearing in the CNF script should in that order also be created.
+ tie %tables, "Tie::IxHash";
+ tie %views, "Tie::IxHash";
+ bless $self, $class;
+}
+
+
+sub isPostgreSQL{shift; return $isPostgreSQL}
+
+##
+# Required to be called when using CNF with an database based storage.
+# This subrotine is also a good example why using generic driver is not recomended.
+# Various SQL db server flavours meta info is def. handled differently and not updated in them.
+#
+# $map - In general is binding of an CNF table to its DATA property, header of the DATA instructed property is self column resolving.
+# If assinged to an array the first element must contain the name,
+# @TODO 20231018 - Specifications page to be provided with examples for this.
+#
+sub initDatabase { my($self, $db, $do_not_auto_synch, $map, $st) = @_;
+#Check and set CNF_CONFIG
+try{
+ $hasRecords = 0;
+ $isPostgreSQL = $db-> get_info( 17) eq 'PostgreSQL';
+ if($isPostgreSQL){
+ my @tbls = $db->tables(undef, 'public'); #<- This is the proper way, via driver, doesn't work on sqlite.
+ foreach (@tbls){
+ my $t = uc substr($_,7); $t =~ s/^["']|['"]$//g;
+ $curr_tables{$t} = 1;
+ }
+ }
+ else{
+ my $pst = selectRecords($self, $db, "SELECT name FROM sqlite_master WHERE type='table' or type='view';");
+ while(my @r = $pst->fetchrow_array()){
+ $curr_tables{$r[0]} = 1;
+ }
+ }
+
+ if(!$curr_tables{CNF_CONFIG}){
+ my $stmt;
+ if($isPostgreSQL){
+ $stmt = qq|
+ CREATE TABLE CNF_CONFIG
+ (
+ NAME character varying(32) NOT NULL,
+ VALUE character varying(128) NOT NULL,
+ DESCRIPTION character varying(256),
+ CONSTRAINT CNF_CONFIG_pkey PRIMARY KEY (NAME)
+ )|;
+ }else{
+ $stmt = qq|
+ CREATE TABLE CNF_CONFIG (
+ NAME VCHAR(16) NOT NULL,
+ VALUE VCHAR(128) NOT NULL,
+ DESCRIPTION VCHAR(256)
+ )|;
+ }
+ $db->begin_work();
+ $db->do($stmt);
+ $self->{parser}->log("CNFParser-> Created CNF_CONFIG table.");
+ $st = $db->prepare('INSERT INTO CNF_CONFIG VALUES(?,?,?);');
+ foreach my $key(sort keys %{$self->{parser}}){
+ my ($dsc,$val);
+ $val = $self->{parser}->const($key);
+ if(ref($val) eq ''){
+ my @sp = split '`', $val;
+ if(scalar @sp>1){$val=$sp[0];$dsc=$sp[1];}else{$dsc=""}
+ $st->execute($key,$val,$dsc);
+ }
+ }
+ $db->commit();
+ }else{ unless ($do_not_auto_synch){
+ my $sel = $db->prepare("SELECT VALUE FROM CNF_CONFIG WHERE NAME LIKE ?;");
+ my $ins = $db->prepare('INSERT INTO CNF_CONFIG VALUES(?,?,?);');
+ foreach my $key(sort keys %{$self->{parser}}){
+ my ($dsc,$val);
+ $val = $self->{parser}->const($key);
+ if(ref($val) eq ''){
+ $sel->execute($key);
+ my @a = $sel->fetchrow_array();
+ if(@a==0){
+ my @sp = split '`', $val;
+ if(scalar @sp>1){$val=$sp[0];$dsc=$sp[1];}else{$dsc=""}
+ $ins->execute($key,$val,$dsc);
+ }
+ }
+ }
+ }}
+ # By default we automatically data insert synchronize script with database state on every init.
+ # If set $do_not_auto_synch = 1 we skip that if table is present, empty or not,
+ # and if has been updated dynamically that is good, what we want. It is of external config. implementation choice.
+ foreach my $tbl(keys %tables){
+ if(!$curr_tables{$tbl}){
+ $st = $tables{$tbl};
+ $self->{parser}->log("CNFParser-> SQL: $st\n");
+ try{
+ $db->do($st);
+ $self->{parser}->log("CNFParser-> Created table: $tbl\n");
+ $do_not_auto_synch = 0;
+ }catch{
+ die "Failed to create:\n$st\nError:$@"
+ }
+ }
+ else{
+ next if $do_not_auto_synch;
+ }
+ }
+ foreach my $tbl(keys %tables){
+ next if $do_not_auto_synch;
+ my @table_info;
+ my $tbl_id_type = $tables_id_type{$tbl};
+ if(isPostgreSQL()){
+ $st = lc $tbl; #we lc, silly psql is lower casing meta and case sensitive for internal purposes.
+ $st="select ordinal_position, column_name, data_type from information_schema.columns where table_schema = 'public' and table_name = '$st';";
+ $self->{parser}->log("CNFParser-> $st", "\n");
+ $st = $db->prepare($st);
+ }else{
+ $st = $db->prepare("pragma table_info($tbl)");
+ }
+ $st->execute();
+ while(my @row_info = $st->fetchrow_array()){
+ $row_info[2] =~ /(\w+)/;
+ $table_info[@table_info] = [$row_info[1], uc $1 ]
+ }
+ my $t = $tbl; my ($sel,$ins,@spec,$q,$qinto);
+ $t = %$map{$t} if $map && %$map{$t};
+ if(ref($t) eq 'ARRAY'){
+ @spec = @$t;
+ $t = $spec[0]; shift @spec;
+ foreach(@spec){ $q.="\"$_\" == ? and " }
+ $q =~ s/\sand\s$//;
+ $st="SELECT * FROM $tbl WHERE $q;";
+ $self->{parser}->log("CNFParser-> $st\n");
+ $sel = $db -> prepare($st);
+ }else{
+ my $prime_key = getPrimaryKeyColumnNameWherePart($db, $tbl);
+ $st="SELECT * FROM $tbl WHERE $prime_key";
+ $self->{parser}->log("CNFParser-> $st\n");
+ $sel = $db -> prepare($st);
+ my @r = $self->selectRecords($db,"select count(*) from $tbl;")->fetchrow_array();
+ $hasRecords = 1 if $r[0] > 0
+ }
+
+ $q = $qinto = ""; my $qa = $tbl_id_type eq 'CNF_INDEX'; foreach(@table_info){
+ if($qa || @$_[0] ne 'ID') {
+ $qinto .="\"@$_[0]\",";
+ $q.="?,"
+ }
+ }
+ $qinto =~ s/,$//;
+ $q =~ s/,$//;
+ $ins = $db -> prepare("INSERT INTO $tbl ($qinto)\nVALUES ($q);");
+
+
+ my $data = $self->{parser} -> {'__DATA__'};
+ if($data){
+ my $data_prp = %$data{$t};
+ if(!$data_prp && $self->{data}){
+ $data_prp = %{$self->{data}}{$t};
+ }
+ if($data_prp){
+ my @hdr;
+ my @rows = @$data_prp;
+ my $auto_increment=0;
+ $db->begin_work();
+ for my $row_idx (0 .. $#rows){
+ my @col = @{$rows[$row_idx]};
+ if($row_idx==0){
+ for my $i(0 .. $#col){
+ $hdr[@hdr]={'_'=>$col[$i],'i'=>$i}
+ }
+ }elsif(@col>0){
+ ##
+ #sel tbl section
+ if(@spec){
+ my @trans = ();
+ foreach my $name (@spec){
+ foreach(@hdr){
+ my $hn = $_->{'_'};
+ my $hi = $_->{'i'};
+ if($name =~ m/ID/i){
+ if($col[$hi]){
+ $trans[@trans] = $col[$hi];
+ }else{
+ $trans[@trans] = $row_idx; # The row index is ID as default on autonumbered ID columns.
+ }
+ last
+ }elsif($name =~ m/$hn/i){
+ $trans[@trans] = $col[$hi];
+ last
+ }
+ }
+ }
+ next if @trans && hasEntry($sel, \@trans);
+ }else{
+ next if hasEntry($sel, $row_idx); # ID is assumed autonumbered on by default
+ }
+ ##
+ my @ins = ();
+ foreach(@hdr){
+ my $hn = $_->{'_'};
+ my $hi = $_->{'i'};
+ for my $i(0 .. $#table_info){
+ if ($table_info[$i][0] =~ m/$hn/i){
+ if($table_info[$i][0]=~/ID/i){
+ if($col[$hi]){
+ $ins[$i] = $col[$hi];
+ }else{
+ $ins[$i] = $row_idx; # The row index is ID as default on autonumbered ID columns.
+ }
+ $auto_increment=$i+1 if $tbl_id_type eq 'AUTOINCREMENT';
+ }else{
+ my $v = $col[$hi];
+ if($table_info[$i][1] =~ /TIME/ || $table_info[$i][1] =~ /DATE/){
+ $TZ = exists $self->{parser}->{'TZ'} ? $self->{parser}->{'TZ'} : CNFDateTime::DEFAULT_TIME_ZONE() if !$TZ;
+ if($v && $v !~ /now|today/i){
+ if($self->{STRICT}&&$v!~/^\d\d\d\d-\d\d-\d\d/){
+ $self-> warn("Invalid date format: $v expecting -> YYYY-MM-DD at start as possibility of DD-MM-YYYY or MM-DD-YYYY is ambiguous.")
+ }
+ $v = CNFDateTime::_toCNFDate($v,$TZ) -> toTimestamp()
+ }else{
+ $v = CNFDateTime->new({TZ=>$TZ}) -> toTimestamp()
+ }
+ }elsif($table_info[$i][1] =~ m/^BOOL/){
+ $v = CNFParser::_isTrue($v) ?1:0;
+ }
+ $ins[$i] = $v
+ }
+ last;
+ }
+ }
+ }
+ $self->{parser}->log("CNFParser-> Insert into $tbl -> ". join(',', @ins)."\n");
+ if($auto_increment){
+ $auto_increment--;
+ splice @ins, $auto_increment, 1
+ }
+ $ins->execute(@ins);
+ }
+ }
+ $db->commit()
+ }else{
+ $self->{parser}->log("CNFParser-> No data collection is available for $tbl\n");
+ }
+ }else{
+ $self->{parser}->log("CNFParser-> No data collection scanned for $tbl\n");
+ }
+
+ }
+
+ foreach my $view(keys %views){
+ if(!$curr_tables{$view}){
+ $st = $views{$view};
+ $self->{parser}->log("CNFParser-> SQL: $st\n");
+ $db->do($st);
+ $self->{parser}->log("CNFParser-> Created view: $view\n")
+ }
+ }
+ undef %tables; undef %tables_id_type;
+ undef %views;
+}
+catch{
+ CNFSQLException->throw(error=>$@, show_trace=>1);
+}
+return $self->{parser}-> const('$RELEASE_VER');
+}
+
+sub _connectDB {
+ my ($user, $pass, $source, $store, $path) = @_;
+ if($path && ! -e $path){
+ $path =~ s/^\.\.\/\.\.\///g;
+ }else{
+ $path = ""
+ }
+ my $DSN = $source .'dbname='.$path.$store;
+ try{
+ return DBI->connect($DSN, $user, $pass, {AutoCommit => 1, RaiseError => 1, PrintError => 0, show_trace=>1});
+ }catch{
+ die "<p>Error->$@</p><br><pre>DSN: $DSN</pre>";
+ }
+}
+sub _credentialsToArray{
+ return split '/', shift
+}
+
+sub createTable { my ($self, $name, $body, $idType) = @_;
+ $tables{$name} = "CREATE TABLE $name(\n$body);";
+ $tables_id_type{$name} = $idType;
+}
+sub createView { my ($self, $name, $body) = @_;
+ $views{$name} = "CREATE VIEW $name AS $body;"
+}
+sub createIndex { my ($self, $body) = @_;
+ my $st = "CREATE INDEX $body;";
+ push @sql, $st;
+}
+sub migrate { my ($self, $name, $value) = @_;
+ my @m = $mig{$name};
+ @m = () if(!@m);
+ push @m, $value;
+ $mig{$name} = [@m];
+}
+sub addStatement { my ($self, $name, $value) = @_;
+ $self->{$name}=$value;
+}
+sub getStatement { my ($self, $name) = @_;
+ return $self->{$name} if exists $self->{$name};
+ return;
+}
+sub hasEntry{ my ($sel, $uid) = @_;
+ return 0 if !$hasRecords;
+ if(ref($uid) eq 'ARRAY'){
+ $sel -> execute(@$uid)
+ }else{
+ $uid=~s/^["']|['"]$//g;
+ $sel -> execute($uid)
+ }
+ my @r=$sel->fetchrow_array();
+ return scalar(@r);
+}
+
+sub getPrimaryKeyColumnNameWherePart { my ($db,$tbl) = @_; $tbl = lc $tbl;
+ my $sql = $isPostgreSQL ?
+qq(SELECT a.attname, format_type(a.atttypid, a.atttypmod) AS data_type
+FROM pg_index i
+JOIN pg_attribute a ON a.attrelid = i.indrelid
+ AND a.attnum = ANY(i.indkey)
+WHERE i.indrelid = '$tbl'::regclass
+AND i.indisprimary;) :
+
+qq(PRAGMA table_info($tbl););
+
+
+my $st = $db->prepare($sql); $st->execute();
+my @r = $st->fetchrow_array();
+if(!@r){
+ CNFSQLException->throw(error=> "Table missing or has no Primary Key -> $tbl", show_trace=>1);
+}
+ if($isPostgreSQL){
+ return "\"$r[0]\"=?";
+ }else{
+ # sqlite
+ # cid[0]|name|type|notnull|dflt_value|pk<--[5]
+ while(!$r[5]){
+ @r = $st->fetchrow_array();
+ if(!@r){
+ CNFSQLException->throw(error=> "Table has no Primary Key -> $tbl", show_trace=>1);
+ }
+ }
+ return $r[1]."=?";
+ }
+}
+
+sub selectRecords {
+ my ($self, $db, $sql) = @_;
+ if(scalar(@_) < 2){
+ die "Wrong number of arguments, expecting CNFParser::selectRecords(\$db, \$sql) got Settings::selectRecords('@_').\n";
+ }
+ try{
+ my $pst = $db->prepare($sql);
+ return 0 if(!$pst);
+ $pst->execute();
+ return $pst;
+ }catch{
+ CNFSQLException->throw(error=>"Database error encountered!\n ERROR->$@\n SQL-> $sql DSN:".$db, show_trace=>1);
+ }
+}
+#@deprecated
+sub tableExists { my ($self, $db, $tbl) = @_;
+ try{
+ $db->do("select count(*) from $tbl;");
+ return 1;
+ }catch{}
+ return 0;
+}
+###
+# Buffer loads initiated a file for sql data instructions.
+# TODO 2020-02-13 Under development.
+#
+sub initLoadDataFile {# my($self, $path) = @_;
+return 0;
+}
+###
+# Reads next collection of records into buffer.
+# returns 2 if reset with new load.
+# returns 1 if done reading data tag value, last block.
+# returns 0 if done reading file, same as last block.
+# readNext is accessed in while loop,
+# filling in a block of the value for a given CNF tag value.
+# Calling readNext, will clear the previous block of data.
+# TODO 2020-02-13 Under development.
+#
+sub readNext(){
+return 0;
+}
+
+sub END {
+undef %tables;undef %views;
+}
+
+1;
+
+=begin copyright
+Programed by : Will Budic
+EContactHash : 990MWWLWM8C2MI8K (https://github.com/wbudic/EContactHash.md)
+Source : https://github.com/wbudic/PerlCNF.git
+Documentation : Specifications_For_CNF_ReadMe.md
+ This source file is copied and usually placed in a local directory, outside of its repository project.
+ So it could not be the actual or current version, can vary or has been modiefied for what ever purpose in another project.
+ Please leave source of origin in this file for future references.
+Open Source Code License -> https://github.com/wbudic/PerlCNF/blob/master/ISC_License.md
+=cut copyright
\ No newline at end of file
--- /dev/null
+package ContactsPlugin;
+
+use strict;
+use warnings;
+no warnings qw(experimental::signatures);
+use feature qw(signatures);
+use Syntax::Keyword::Try;
+use Clone qw(clone);
+use DBI;
+
+use constant VERSION => '1.0';
+
+CNFParser::import();
+
+sub new ($class, $plugin){
+ my $settings;
+ if($plugin){
+ $settings = clone $plugin; #clone otherwise will get hijacked with blessings.
+ }
+ return bless $settings, $class
+}
+
+###
+# Process config data to contain expected fields.
+###
+sub process ($self, $parser, $property) {
+ $self->{date} = now();
+ my %S = $parser-> collection('%WEB_SERVICE_SETTINGS');
+ my $SQL = $parser-> SQL();
+ my $db = CNFSQL::_connectDB($S{user},$S{pass},$S{dbi_source},$S{dbi_store},$S{LOG_PATH});
+ # The mighty CNFParser reads the plugin CNF property and sets our plugin, to scripted attribute bindings, for sql table and data.
+ $SQL -> initiDatabase($db,0,{$self->{table}=>$self->{data}});
+ my %hdr = $SQL -> getDataHeaderMap($self->{table});
+ my $cgi = $parser -> {CGI};
+ my $action = $cgi -> param('action');
+ my $buffer;
+
+ if($action eq 'form'){
+ my $st = $SQL -> selectRecords($db,"select * from $self->{table}");
+
+ $buffer = js().css().qq(
+ <p class="styled-message">Please select who you wish to send a contact message from the following table.<p>
+ <table class="styled-table" style="width:fit-content;">
+ <thead>
+ <td>ID</td>
+ <td>Contact</td>
+ <td>ECHASH</td>
+ </thead>
+ <tbody>
+ );
+ while (my @row = $st->fetchrow_array()){
+ my $details = $row[$hdr{Details}]; $details = "" if not $details;#driver returns empty string as undef from table.
+ $buffer .= qq(
+ <tr>
+ <td>$row[$hdr{ID}]</td>
+ <td><a onclick="return contact('$row[$hdr{Alias}]','$row[$hdr{ECHSH}]')">$row[$hdr{Alias}]<a></td>
+ <td><a onclick="return contact('$row[$hdr{Alias}]','$row[$hdr{ECHSH}]')">$row[$hdr{ECHSH}]</a></td>
+ </tr>
+ <tr><td colspan="3">$details</td></tr>
+ )
+ }
+ $buffer .= "</tbody>\n</table>";
+ $buffer .= qq( <div id="div_contact" class="styled-table"
+ style="text-align: left; padding: 15px; visibility:hidden">
+ <form id="frm_contact" action="CNFServices.cgi" method="post">
+ <input type="hidden" name="service" value="contacts"/>
+ <input type="hidden" name="action" value="contact"/>
+ <label for="poster_name">Your Name:</label>
+ <input type="text" id="poster_name" name="poster_name" value="" required><br><br>
+ <label for="poster_email">Your Email:</label>
+ <input type="text" id="poster_email" name="poster_email" value="" required><br><br>
+ <label for="poster_contacts">You are Contacting:</label>
+ <input type="text" id="poster_contacts" name="poster_contacts" value=""
+ style="width:70px;" readonly>
+ <input type="text" name="echsh" value="" readonly/><br><br>
+ <label for="post">Your Message:</label><br><br>
+
+ <textarea name="post" rows="10" autocorrect="on" required></textarea><br><br>
+
+ <input type="submit" value="Submit" class="ui-button">
+ </form>
+ </div>
+ );
+ }
+ $parser->data()->{PAGE} = \$buffer;
+
+ #$parser->data()->{$property} = \@data
+}
+sub js{
+<<JS
+<script>
+
+
+</script>
+JS
+}
+sub css{
+<<CSS
+<style>
+ .styled-table {
+ display: block;
+ border-collapse: collapse;
+ margin: 25px 0;
+ font-size: 0.9em;
+ font-family: sans-serif;
+ box-shadow: 8px 10px 20px rgba(0, 0, 0, 0.572);
+}
+.styled-table thead tr {
+ background-color: #00fbff;
+ color: #121111;
+ text-align: left;
+ font-weight: bold;
+}
+
+.styled-table td{
+ border:1px solid black;padding: 5px;margin: 0;
+ padding: 12px 15px;
+}
+.styled-table a{
+ cursor:pointer;
+}
+.styled-table tbody tr {
+ border-bottom: 1px solid #eff190;
+}
+.styled-table tbody tr:nth-of-type(even) {
+ background-color: #f9b5b5;
+}
+.styled-table tbody tr:last-of-type {
+ border-bottom: 1px solid #000000;
+ border-right: 1px solid #000000;
+}
+.styled-table tbody tr:hover {
+ font-weight: bold;
+ background-color: #98fce8;
+}
+.styled-table textarea{
+ overflow-y: scroll;
+ overflow: scroll;
+ width:98%;
+}
+.styled-message{
+ margin-left: 0;
+ font-weight: bold;
+ text-shadow: 8px 10px 20px rgba(0, 0, 0, 0.572)}
+</style>
+CSS
+}
+
+1;
+
+
+=begin copyright
+Programed by : Will Budic
+EContactHash : 990MWWLWM8C2MI8K (https://github.com/wbudic/EContactHash.md)
+Source : https://github.com/wbudic/PerlCNF.git
+Documentation : Specifications_For_CNF_ReadMe.md
+ This source file is copied and usually placed in a local directory, outside of its repository project.
+ So it could not be the actual or current version, can vary or has been modiefied for what ever purpose in another project.
+ Please leave source of origin in this file for future references.
+Open Source Code License -> https://github.com/wbudic/PerlCNF/blob/master/ISC_License.md
+=cut copyright
\ No newline at end of file
$ln =~ s/^\s*\>+//;
($ln =~ /^(\s+) (\d+) \.\s (.*)/x || $ln =~ /^(\s*) ([-+*]) \s(.*)/x);
if($2 && $2 =~ /[-+*]/){
- $bqte_tag = "ul";
+ $bqte_tag = "ul"
}elsif($2){
- $bqte_tag = "ol";
+ $bqte_tag = "ol"
}else{
- $bqte_tag = "p";
+ $bqte_tag = "p"
}
if(!$bqte_nested){
$bqte_nested = $nested;
my $feed = $cgi->param('feed') if $cgi;
$parser->log("Feed request:$feed");
for my $idx (0 .. $#data){
- my @col = @{$data[$idx]};
+ my @col = @{$data[$idx]};
if($idx==0){
for my $i(0..$#col){ # Get the matching table column index names as scripted.
$hdr{$col[$i]} = $i
);
pnl.show();
pnl.css('visibility','visible');
+ if(ID === "#feeds"){
$(document).scrollTop( $("#rss_anchor").offset().top );
+ }
$.post('CNFServices.cgi', {service:'feeds',action:'list'}, displayFeeds).fail(
function(response) {
pnl.html(response.responseText);
$.post('CNFServices.cgi', {service:'feeds', action:'read', feed:feed}, displayFeeds).fail(
function(response) {
pnl.html(response.responseText);
- pnl.fadeOut(20000);
+ pnl.fadeOut(30000);
}
);
}
pnl.html(content);
$("#index-content").css("height",'100%');
pnl.show();
- $(document).scrollTop( $("#rss_anchor").offset().top );
+ if(ID === "#feeds"){
+ $(document).scrollTop( $("#rss_anchor").offset().top );
+ }
}
function demoLogin() {
$('#frm_login input[name=passw]').val("admin");
form.submit();
}
+
+function contactForm(){
+ var pnl = $(ID);
+ $.post('CNFServices.cgi', {service:'contacts', action:'form'},
+ function(content){
+ pnl.html(content);
+ pnl.css('visibility','visible');
+ pnl.show();
+ }).fail(
+ function(response) {
+ pnl.html(response.responseText);
+ pnl.fadeOut(30000);
+ }
+ );
+}
+function contact(contact,echsh){
+ $("#frm_contact input[name=poster_contacts]").val(contact);
+ $("#frm_contact input[name=echsh]").val(echsh);
+ var pnl = $('#div_contact');
+ pnl.show();
+ pnl.css('visibility','visible');
+ return false;
+}
\ No newline at end of file
padding:0;
}
p {
- font-family: Bookman;
- margin-left: 70px;
- font-weight: normal;
+ margin-left: 70px;
+ font-weight: bold;
}
pre{
color: #070707;
background: #ccffff;
margin-top: 15px;
margin-left: 200px;
- width: 400px;
+ width: 280px;
text-decoration-style: wavy;
}
border: 1px solid black;
border-right: 1px solid black;
text-align: center;
+ margin:0;
+ padding: 3px;
}
.edit {
margin-left: 91%;
}
+#menu_page {
+ border: 2px solid #10c69b72;
+}
+.marg-top-2{
+ margin-top: 5px;
+}
+
#frm_login {
vertical-align: middle;
margin: 0;
}
+.ui-icon,.menu_title {
+ background-color: transparent;
+ font-weight: bolder;
+}
+.ui-button {
+ font-size: 12px !important;
+ outline-color: transparent;
+}
+.ui-button:hover {
+ background: #00ddfa79;
+ background-image: none;
+ color: #000000 !important;
+}
+.ui-selectmenu-button:{
+ background-image: none;
+}
+.ui-selectmenu-button:hover{
+ background-image: none;
+ background: #00ddfa;
+}
+
-.ui-button,
-.ui-button-text .ui-button {
- font-size: 12px !important;
-}
+
+.ui-widget.ui-widget-content {
+ border: 1px solid #131311;
+ color: #362b36;
+ font-weight: bold;
+}
+.ui-widget-content {
+ background: #183c76;
+ color: #362b36;
+}
.ui-menu {
list-style: none;
padding: 10px;
+<head>
+<style>
+.textual {
+ width: 40%;
+}
+.textual p::first-letter {
+ color: blueviolet;
+ initial-letter: 3 2;
+ padding-right: 5pt;
+}
+</style>
+<style>
+ .styled-table {
+ border-collapse: collapse;
+ margin: 25px 0;
+ font-size: 0.9em;
+ font-family: sans-serif;
+ min-width: 80%;
+ box-shadow: 8px 10px 20px rgba(0, 0, 0, 0.572);
+}
+.styled-table thead tr {
+ background-color: #00fbff;
+ color: #121111;
+ text-align: left;
+}
+
+.styled-table td{
+ border:1px solid black;padding: 5px;margin: 0;
+ padding: 12px 15px;
+}
+.styled-table tbody tr {
+ border-bottom: 1px solid #eff190;
+}
+.styled-table tbody tr:nth-of-type(even) {
+ background-color: #f9b5b5;
+}
+.styled-table tbody tr:last-of-type {
+ border-bottom: 1px solid #000000;
+ border-right: 1px solid #000000;
+}
+.styled-table tbody tr:hover {
+ font-weight: bold;
+ background-color: #98fce8;
+}
+</style>
+</head>
<body>
<h1>Hello From THTTPD</h1>
+<table class="styled-table">
+ <thead>
+ <td>ID</td>
+ <td>Contact</td>
+ <td>ECHASH</td>
+ </thead>
+ <tbody>
+ <tr>
+ <td>1</td>
+ <td>webadmin</td>
+ <td>990MWWLWM8C2MI8K</td>
+ </tr>
+ <tr><td colspan="3">A </td></tr>
+ </tbody>
+</table>
+
+
+
+
+<div class="clear"></div>
<b>To run the Web Application please click on <a href="cgi-bin">CGI-BIN</a>.</b>
+<div class="textual">
+<p>MaryLou wore the tiara with real pride.
+There was something that made doing anything she didn't really want to do a bit easier when she wore it.
+ She really didn't care what those staring through the window were thinking as she vacuumed her apartment.
+</p>
+</div>
+<p>
+ The water rush down the wash and into the slot canyon below. Two hikers had started the day to sunny weather without a cloud in the sky, but they hadn't thought to check the weather north of the canyon. Huge thunderstorms had brought a deluge o rain and produced flash floods heading their way. The two hikers had no idea what was coming.
+</p><p>
+ There are different types of secrets. She had held onto plenty of them during her life, but this one was different. She found herself holding onto the worst type. It was the type of secret that could gnaw away at your insides if you didn't tell someone about it, but it could end up getting you killed if you did.
+</p>
+
+</p>
</body>
--- /dev/null
+sudo apt install cpanminus -y
+sudo apt install perlbrew -y
+sudo apt install libperl-critic-perl -y
+sudo apt install libperl-dev -y
+Avoid: sudo apt install openssl -y
+sudo cpan DBD::SQLite;
+sudo cpan Perl::LanguageServer
+# before was -> sudo cpanm Try::Tiny;
+sudo cpan Exception::Class
+sudo cpan Log::Log4per
+sudo cpan Syntax::Keyword::Try
+sudo cpan DateTime;
+sudo cpan DateTime::Format::Human::Duration
+sudo cpan DateTime::Format::SQLite;
+sudo cpan Text::CSV;
+sudo cpan Number::Bytes::Human;
+sudo cpan Gzip::Faster;
+sudo cpan Number/Bytes/Human.pm;
+sudo cpan Regexp::Common;
+sudo cpan JSON;
+sudo cpan Switch;
+sudo cpan IPC::Run;
+sudo cpan Syntax::Keyword::Try;
+sudo cpan Text::Markdown
+sudo cpan Crypt::Blowfish;
+sudo cpan Compress::Zlib;
+sudo cpan IO::Compress::Gzip;
+sudo cpan IO::Prompter;
+sudo cpan IO::Interactive;
+sudo cpan -T DBD::Pg;
+sudo cpan CGI CGI::Session;
+sudo cpan Crypt::CBC;
+sudo cpan DBI;
+sudo cpan DBD::SQLite;
+ *$ sudo cpan -r Perl::LanguageServer
--- /dev/null
+GIT-STASH(1) Git Manual GIT-STASH(1)
+
+N\bNA\bAM\bME\bE
+ git-stash - Stash the changes in a dirty working directory away
+
+S\bSY\bYN\bNO\bOP\bPS\bSI\bIS\bS
+ _\bg_\bi_\bt _\bs_\bt_\ba_\bs_\bh list [<log-options>]
+ _\bg_\bi_\bt _\bs_\bt_\ba_\bs_\bh show [-u|--include-untracked|--only-untracked] [<diff-options>] [<stash>]
+ _\bg_\bi_\bt _\bs_\bt_\ba_\bs_\bh drop [-q|--quiet] [<stash>]
+ _\bg_\bi_\bt _\bs_\bt_\ba_\bs_\bh ( pop | apply ) [--index] [-q|--quiet] [<stash>]
+ _\bg_\bi_\bt _\bs_\bt_\ba_\bs_\bh branch <branchname> [<stash>]
+ _\bg_\bi_\bt _\bs_\bt_\ba_\bs_\bh [push [-p|--patch] [-k|--[no-]keep-index] [-q|--quiet]
+ [-u|--include-untracked] [-a|--all] [-m|--message <message>]
+ [--pathspec-from-file=<file> [--pathspec-file-nul]]
+ [--] [<pathspec>...]]
+ _\bg_\bi_\bt _\bs_\bt_\ba_\bs_\bh clear
+ _\bg_\bi_\bt _\bs_\bt_\ba_\bs_\bh create [<message>]
+ _\bg_\bi_\bt _\bs_\bt_\ba_\bs_\bh store [-m|--message <message>] [-q|--quiet] <commit>
+
+D\bDE\bES\bSC\bCR\bRI\bIP\bPT\bTI\bIO\bON\bN
+ Use g\bgi\bit\bt s\bst\bta\bas\bsh\bh when you want to record the current state of the working directory and the index, but want to go back to a clean working directory. The command saves your local modifications away and reverts the working directory to match the H\bHE\bEA\bAD\bD commit.
+
+ The modifications stashed away by this command can be listed with g\bgi\bit\bt s\bst\bta\bas\bsh\bh l\bli\bis\bst\bt, inspected with g\bgi\bit\bt s\bst\bta\bas\bsh\bh s\bsh\bho\bow\bw, and restored (potentially on top of a different commit) with g\bgi\bit\bt s\bst\bta\bas\bsh\bh a\bap\bpp\bpl\bly\by. Calling g\bgi\bit\bt s\bst\bta\bas\bsh\bh without any arguments is equivalent to g\bgi\bit\bt s\bst\bta\bas\bsh\bh p\bpu\bus\bsh\bh. A stash is by
+ default listed as "WIP on _\bb_\br_\ba_\bn_\bc_\bh_\bn_\ba_\bm_\be ...", but you can give a more descriptive message on the command line when you create one.
+
+ The latest stash you created is stored in r\bre\bef\bfs\bs/\b/s\bst\bta\bas\bsh\bh; older stashes are found in the reflog of this reference and can be named using the usual reflog syntax (e.g. s\bst\bta\bas\bsh\bh@\b@{\b{0\b0}\b} is the most recently created stash, s\bst\bta\bas\bsh\bh@\b@{\b{1\b1}\b} is the one before it, s\bst\bta\bas\bsh\bh@\b@{\b{2\b2.\b.h\bho\bou\bur\brs\bs.\b.a\bag\bgo\bo}\b} is also
+ possible). Stashes may also be referenced by specifying just the stash index (e.g. the integer n\bn is equivalent to s\bst\bta\bas\bsh\bh@\b@{\b{n\bn}\b}).
+
+C\bCO\bOM\bMM\bMA\bAN\bND\bDS\bS
+ push [-p|--patch] [-k|--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q|--quiet] [-m|--message <message>] [--pathspec-from-file=<file> [--pathspec-file-nul]] [--] [<pathspec>...]
+ Save your local modifications to a new _\bs_\bt_\ba_\bs_\bh _\be_\bn_\bt_\br_\by and roll them back to HEAD (in the working tree and in the index). The <message> part is optional and gives the description along with the stashed state.
+
+ For quickly making a snapshot, you can omit "push". In this mode, non-option arguments are not allowed to prevent a misspelled subcommand from making an unwanted stash entry. The two exceptions to this are s\bst\bta\bas\bsh\bh -\b-p\bp which acts as alias for s\bst\bta\bas\bsh\bh p\bpu\bus\bsh\bh -\b-p\bp and pathspec
+ elements, which are allowed after a double hyphen -\b--\b- for disambiguation.
+
+ save [-p|--patch] [-k|--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q|--quiet] [<message>]
+ This option is deprecated in favour of _\bg_\bi_\bt _\bs_\bt_\ba_\bs_\bh _\bp_\bu_\bs_\bh. It differs from "stash push" in that it cannot take pathspec. Instead, all non-option arguments are concatenated to form the stash message.
+
+ list [<log-options>]
+ List the stash entries that you currently have. Each _\bs_\bt_\ba_\bs_\bh _\be_\bn_\bt_\br_\by is listed with its name (e.g. s\bst\bta\bas\bsh\bh@\b@{\b{0\b0}\b} is the latest entry, s\bst\bta\bas\bsh\bh@\b@{\b{1\b1}\b} is the one before, etc.), the name of the branch that was current when the entry was made, and a short description of the commit the
+ entry was based on.
+
+ stash@{0}: WIP on submit: 6ebd0e2... Update git-stash documentation
+ stash@{1}: On master: 9cc0589... Add git-stash
+
+ The command takes options applicable to the _\bg_\bi_\bt _\bl_\bo_\bg command to control what is shown and how. See g\bgi\bit\bt-\b-l\blo\bog\bg(1).
+
+ show [-u|--include-untracked|--only-untracked] [<diff-options>] [<stash>]
+ Show the changes recorded in the stash entry as a diff between the stashed contents and the commit back when the stash entry was first created. By default, the command shows the diffstat, but it will accept any format known to _\bg_\bi_\bt _\bd_\bi_\bf_\bf (e.g., g\bgi\bit\bt s\bst\bta\bas\bsh\bh s\bsh\bho\bow\bw -\b-p\bp s\bst\bta\bas\bsh\bh@\b@{\b{1\b1}\b} to
+ view the second most recent entry in patch form). If no <\b<d\bdi\bif\bff\bf-\b-o\bop\bpt\bti\bio\bon\bn>\b> is provided, the default behavior will be given by the s\bst\bta\bas\bsh\bh.\b.s\bsh\bho\bow\bwS\bSt\bta\bat\bt, and s\bst\bta\bas\bsh\bh.\b.s\bsh\bho\bow\bwP\bPa\bat\btc\bch\bh config variables. You can also use s\bst\bta\bas\bsh\bh.\b.s\bsh\bho\bow\bwI\bIn\bnc\bcl\blu\bud\bde\beU\bUn\bnt\btr\bra\bac\bck\bke\bed\bd to set whether -\b--\b-i\bin\bnc\bcl\blu\bud\bde\be-\b-u\bun\bnt\btr\bra\bac\bck\bke\bed\bd is enabled by
+ default.
+
+ pop [--index] [-q|--quiet] [<stash>]
+ Remove a single stashed state from the stash list and apply it on top of the current working tree state, i.e., do the inverse operation of g\bgi\bit\bt s\bst\bta\bas\bsh\bh p\bpu\bus\bsh\bh. The working directory must match the index.
+
+ Applying the state can fail with conflicts; in this case, it is not removed from the stash list. You need to resolve the conflicts by hand and call g\bgi\bit\bt s\bst\bta\bas\bsh\bh d\bdr\bro\bop\bp manually afterwards.
+
+ apply [--index] [-q|--quiet] [<stash>]
+ Like p\bpo\bop\bp, but do not remove the state from the stash list. Unlike p\bpo\bop\bp, <\b<s\bst\bta\bas\bsh\bh>\b> may be any commit that looks like a commit created by s\bst\bta\bas\bsh\bh p\bpu\bus\bsh\bh or s\bst\bta\bas\bsh\bh c\bcr\bre\bea\bat\bte\be.
+
+ branch <branchname> [<stash>]
+ Creates and checks out a new branch named <\b<b\bbr\bra\ban\bnc\bch\bhn\bna\bam\bme\be>\b> starting from the commit at which the <\b<s\bst\bta\bas\bsh\bh>\b> was originally created, applies the changes recorded in <\b<s\bst\bta\bas\bsh\bh>\b> to the new working tree and index. If that succeeds, and <\b<s\bst\bta\bas\bsh\bh>\b> is a reference of the form
+ s\bst\bta\bas\bsh\bh@\b@{\b{<\b<r\bre\bev\bvi\bis\bsi\bio\bon\bn>\b>}\b}, it then drops the <\b<s\bst\bta\bas\bsh\bh>\b>.
+
+ This is useful if the branch on which you ran g\bgi\bit\bt s\bst\bta\bas\bsh\bh p\bpu\bus\bsh\bh has changed enough that g\bgi\bit\bt s\bst\bta\bas\bsh\bh a\bap\bpp\bpl\bly\by fails due to conflicts. Since the stash entry is applied on top of the commit that was HEAD at the time g\bgi\bit\bt s\bst\bta\bas\bsh\bh was run, it restores the originally stashed state with no
+ conflicts.
+
+ clear
+ Remove all the stash entries. Note that those entries will then be subject to pruning, and may be impossible to recover (see _\bE_\bx_\ba_\bm_\bp_\bl_\be_\bs below for a possible strategy).
+
+ drop [-q|--quiet] [<stash>]
+ Remove a single stash entry from the list of stash entries.
+
+ create
+ Create a stash entry (which is a regular commit object) and return its object name, without storing it anywhere in the ref namespace. This is intended to be useful for scripts. It is probably not the command you want to use; see "push" above.
+
+ store
+ Store a given stash created via _\bg_\bi_\bt _\bs_\bt_\ba_\bs_\bh _\bc_\br_\be_\ba_\bt_\be (which is a dangling merge commit) in the stash ref, updating the stash reflog. This is intended to be useful for scripts. It is probably not the command you want to use; see "push" above.
+
+O\bOP\bPT\bTI\bIO\bON\bNS\bS
+ -a, --all
+ This option is only valid for p\bpu\bus\bsh\bh and s\bsa\bav\bve\be commands.
+
+ All ignored and untracked files are also stashed and then cleaned up with g\bgi\bit\bt c\bcl\ble\bea\ban\bn.
+
+ -u, --include-untracked, --no-include-untracked
+ When used with the p\bpu\bus\bsh\bh and s\bsa\bav\bve\be commands, all untracked files are also stashed and then cleaned up with g\bgi\bit\bt c\bcl\ble\bea\ban\bn.
+
+ When used with the s\bsh\bho\bow\bw command, show the untracked files in the stash entry as part of the diff.
+
+ --only-untracked
+ This option is only valid for the s\bsh\bho\bow\bw command.
+
+ Show only the untracked files in the stash entry as part of the diff.
+
+ --index
+ This option is only valid for p\bpo\bop\bp and a\bap\bpp\bpl\bly\by commands.
+
+ Tries to reinstate not only the working tree’s changes, but also the index’s ones. However, this can fail, when you have conflicts (which are stored in the index, where you therefore can no longer apply the changes as they were originally).
+
+ -k, --keep-index, --no-keep-index
+ This option is only valid for p\bpu\bus\bsh\bh and s\bsa\bav\bve\be commands.
+
+ All changes already added to the index are left intact.
+
+ -p, --patch
+ This option is only valid for p\bpu\bus\bsh\bh and s\bsa\bav\bve\be commands.
+
+ Interactively select hunks from the diff between HEAD and the working tree to be stashed. The stash entry is constructed such that its index state is the same as the index state of your repository, and its worktree contains only the changes you selected interactively. The
+ selected changes are then rolled back from your worktree. See the “Interactive Mode” section of g\bgi\bit\bt-\b-a\bad\bdd\bd(1) to learn how to operate the -\b--\b-p\bpa\bat\btc\bch\bh mode.
+
+ The -\b--\b-p\bpa\bat\btc\bch\bh option implies -\b--\b-k\bke\bee\bep\bp-\b-i\bin\bnd\bde\bex\bx. You can use -\b--\b-n\bno\bo-\b-k\bke\bee\bep\bp-\b-i\bin\bnd\bde\bex\bx to override this.
+
+ --pathspec-from-file=<file>
+ This option is only valid for p\bpu\bus\bsh\bh command.
+
+ Pathspec is passed in <\b<f\bfi\bil\ble\be>\b> instead of commandline args. If <\b<f\bfi\bil\ble\be>\b> is exactly -\b- then standard input is used. Pathspec elements are separated by LF or CR/LF. Pathspec elements can be quoted as explained for the configuration variable c\bco\bor\bre\be.\b.q\bqu\buo\bot\bte\beP\bPa\bat\bth\bh (see g\bgi\bit\bt-\b-c\bco\bon\bnf\bfi\big\bg(1)). See
+ also -\b--\b-p\bpa\bat\bth\bhs\bsp\bpe\bec\bc-\b-f\bfi\bil\ble\be-\b-n\bnu\bul\bl and global -\b--\b-l\bli\bit\bte\ber\bra\bal\bl-\b-p\bpa\bat\bth\bhs\bsp\bpe\bec\bcs\bs.
+
+ --pathspec-file-nul
+ This option is only valid for p\bpu\bus\bsh\bh command.
+
+ Only meaningful with -\b--\b-p\bpa\bat\bth\bhs\bsp\bpe\bec\bc-\b-f\bfr\bro\bom\bm-\b-f\bfi\bil\ble\be. Pathspec elements are separated with NUL character and all other characters are taken literally (including newlines and quotes).
+
+ -q, --quiet
+ This option is only valid for a\bap\bpp\bpl\bly\by, d\bdr\bro\bop\bp, p\bpo\bop\bp, p\bpu\bus\bsh\bh, s\bsa\bav\bve\be, s\bst\bto\bor\bre\be commands.
+
+ Quiet, suppress feedback messages.
+
+ --
+ This option is only valid for p\bpu\bus\bsh\bh command.
+
+ Separates pathspec from options for disambiguation purposes.
+
+ <pathspec>...
+ This option is only valid for p\bpu\bus\bsh\bh command.
+
+ The new stash entry records the modified states only for the files that match the pathspec. The index entries and working tree files are then rolled back to the state in HEAD only for these files, too, leaving files that do not match the pathspec intact.
+
+ For more details, see the _\bp_\ba_\bt_\bh_\bs_\bp_\be_\bc entry in g\bgi\bit\btg\bgl\blo\bos\bss\bsa\bar\bry\by(7).
+
+ <stash>
+ This option is only valid for a\bap\bpp\bpl\bly\by, b\bbr\bra\ban\bnc\bch\bh, d\bdr\bro\bop\bp, p\bpo\bop\bp, s\bsh\bho\bow\bw commands.
+
+ A reference of the form s\bst\bta\bas\bsh\bh@\b@{\b{<\b<r\bre\bev\bvi\bis\bsi\bio\bon\bn>\b>}\b}. When no <\b<s\bst\bta\bas\bsh\bh>\b> is given, the latest stash is assumed (that is, s\bst\bta\bas\bsh\bh@\b@{\b{0\b0}\b}).
+
+D\bDI\bIS\bSC\bCU\bUS\bSS\bSI\bIO\bON\bN
+ A stash entry is represented as a commit whose tree records the state of the working directory, and its first parent is the commit at H\bHE\bEA\bAD\bD when the entry was created. The tree of the second parent records the state of the index when the entry is made, and it is made a child of
+ the H\bHE\bEA\bAD\bD commit. The ancestry graph looks like this:
+
+ .----W
+ / /
+ -----H----I
+
+ where H\bH is the H\bHE\bEA\bAD\bD commit, I\bI is a commit that records the state of the index, and W\bW is a commit that records the state of the working tree.
+
+E\bEX\bXA\bAM\bMP\bPL\bLE\bES\bS
+ Pulling into a dirty tree
+ When you are in the middle of something, you learn that there are upstream changes that are possibly relevant to what you are doing. When your local changes do not conflict with the changes in the upstream, a simple g\bgi\bit\bt p\bpu\bul\bll\bl will let you move forward.
+
+ However, there are cases in which your local changes do conflict with the upstream changes, and g\bgi\bit\bt p\bpu\bul\bll\bl refuses to overwrite your changes. In such a case, you can stash your changes away, perform a pull, and then unstash, like this:
+
+ $ git pull
+ ...
+ file foobar not up to date, cannot merge.
+ $ git stash
+ $ git pull
+ $ git stash pop
+
+ Interrupted workflow
+ When you are in the middle of something, your boss comes in and demands that you fix something immediately. Traditionally, you would make a commit to a temporary branch to store your changes away, and return to your original branch to make the emergency fix, like this:
+
+ # ... hack hack hack ...
+ $ git switch -c my_wip
+ $ git commit -a -m "WIP"
+ $ git switch master
+ $ edit emergency fix
+ $ git commit -a -m "Fix in a hurry"
+ $ git switch my_wip
+ $ git reset --soft HEAD^
+ # ... continue hacking ...
+
+ You can use _\bg_\bi_\bt _\bs_\bt_\ba_\bs_\bh to simplify the above, like this:
+
+ # ... hack hack hack ...
+ $ git stash
+ $ edit emergency fix
+ $ git commit -a -m "Fix in a hurry"
+ $ git stash pop
+ # ... continue hacking ...
+
+ Testing partial commits
+ You can use g\bgi\bit\bt s\bst\bta\bas\bsh\bh p\bpu\bus\bsh\bh -\b--\b-k\bke\bee\bep\bp-\b-i\bin\bnd\bde\bex\bx when you want to make two or more commits out of the changes in the work tree, and you want to test each change before committing:
+
+ # ... hack hack hack ...
+ $ git add --patch foo # add just first part to the index
+ $ git stash push --keep-index # save all other changes to the stash
+ $ edit/build/test first part
+ $ git commit -m 'First part' # commit fully tested change
+ $ git stash pop # prepare to work on all other changes
+ # ... repeat above five steps until one commit remains ...
+ $ edit/build/test remaining parts
+ $ git commit foo -m 'Remaining parts'
+
+ Recovering stash entries that were cleared/dropped erroneously
+ If you mistakenly drop or clear stash entries, they cannot be recovered through the normal safety mechanisms. However, you can try the following incantation to get a list of stash entries that are still in your repository, but not reachable any more:
+
+ git fsck --unreachable |
+ grep commit | cut -d\ -f3 |
+ xargs git log --merges --no-walk --grep=WIP
+
+S\bSE\bEE\bE A\bAL\bLS\bSO\bO
+ g\bgi\bit\bt-\b-c\bch\bhe\bec\bck\bko\bou\but\bt(1), g\bgi\bit\bt-\b-c\bco\bom\bmm\bmi\bit\bt(1), g\bgi\bit\bt-\b-r\bre\bef\bfl\blo\bog\bg(1), g\bgi\bit\bt-\b-r\bre\bes\bse\bet\bt(1), g\bgi\bit\bt-\b-s\bsw\bwi\bit\btc\bch\bh(1)
+
+G\bGI\bIT\bT
+ Part of the g\bgi\bit\bt(1) suite
+
+Git 2.34.1 02/08/2023 GIT-STASH(1)