1- // $Id: yacli.c,v 4.12 2026/04/29 23:03:05 bbonev Exp $
1+ // $Id: yacli.c,v 4.13 2026/04/30 06:59:53 bbonev Exp $
22//
33// Copyright © 2015-2020 Boian Bonev (bbonev@ipacct.com) {{{
44//
@@ -140,6 +140,8 @@ struct _yacli {
140140 int moresiz ; // morebuf alloc size
141141 int parsedcnt ; // parsed command word count
142142 int parsedsiz ; // parsed command array size
143+ int hstmax ; // soft cap on history entries; 0 = unlimited
144+ int hstcnt ; // current history entry count
143145 uint8_t more :1 ; // enable paged output
144146 uint8_t redraw :1 ; // prompt needs redraw
145147 uint8_t wastab :1 ; // control for double tab
@@ -214,32 +216,39 @@ inline void yacli_set_showtermsize(yacli *cli,int v) { // {{{
214216 cli -> showtsize = !!v ;
215217} // }}}
216218
217- static char myver []= "\0Yet another command line interface library (https://github.com/bbonev/yacli) $Revision: 4.12 $\n\n" ; // {{{
219+ static char myver []= "\0Yet another command line interface library (https://github.com/bbonev/yacli) $Revision: 4.13 $\n\n" ; // {{{
218220// }}}
219221
220222inline const char * yacli_ver (void ) { // {{{
221223 return myver ;
222224} // }}}
223225
226+ static inline void yacli_buf_zeroterm (yacli * cli );
227+
224228static inline int yacli_buf_inc (char * * buf ,int * siz ,int * len ,int add ) { // {{{
225- // increase buffer by len, if needed
229+ // ensure *siz can hold * len+add bytes (strictly above, so a trailing null fits); growth rounded to BUFFER_STEP
226230 // return 0 on success, non-zero on error
231+ int needed ,alloclen ;
232+ char * n ;
233+
227234 if (add <=0 )
228235 return 0 ; // ok
229- if (* siz <=* len + add ) { // need to realloc
230- int alloclen = * siz + (* len /BUFFER_STEP + 1 )* BUFFER_STEP ;
231- char * n = calloc (1 ,alloclen );
232-
233- if (!n ) // no free mem, nothing more to do
234- return -1 ;
236+ needed = * len + add ;
237+ if (* siz > needed ) // already enough
238+ return 0 ;
239+ alloclen = ((needed + BUFFER_STEP )/BUFFER_STEP )* BUFFER_STEP ; // next BUFFER_STEP-aligned size strictly above needed
240+ if (alloclen <=* siz ) // assure progress when needed lands exactly at a BUFFER_STEP boundary
241+ alloclen = * siz + BUFFER_STEP ;
242+ n = calloc (1 ,alloclen );
243+ if (!n ) // no free mem, nothing more to do
244+ return -1 ;
235245
236- if (* siz && * buf )
237- memcpy (n ,* buf ,* siz );
238- if (* buf )
239- free (* buf );
240- * buf = n ;
241- * siz = alloclen ;
242- }
246+ if (* len && * buf ) // copy only valid contents, not slack
247+ memcpy (n ,* buf ,* len );
248+ if (* buf )
249+ free (* buf );
250+ * buf = n ;
251+ * siz = alloclen ;
243252 return 0 ;
244253} // }}}
245254
@@ -459,7 +468,7 @@ static inline int yacli_filter_feed_include(filter_inst *fltr,const char *line,i
459468 if (!fltr -> next -> fltr -> feed )
460469 return -1 ;
461470
462- if (yacli_buf_inc (& fltr -> buf ,& fltr -> bufsiz ,& fltr -> buflen ,fltr -> buflen + len + 1 )) // +1 for zero term
471+ if (yacli_buf_inc (& fltr -> buf ,& fltr -> bufsiz ,& fltr -> buflen ,len + 1 )) // additional bytes beyond current contents ( +1 for zero term)
463472 return -1 ; // no memory
464473
465474 memcpy (fltr -> buf + fltr -> buflen ,line ,len );
@@ -519,7 +528,7 @@ static inline int yacli_filter_feed_exclude(filter_inst *fltr,const char *line,i
519528 if (!fltr -> next -> fltr -> feed )
520529 return -1 ;
521530
522- if (yacli_buf_inc (& fltr -> buf ,& fltr -> bufsiz ,& fltr -> buflen ,fltr -> buflen + len + 1 )) // +1 for zero term
531+ if (yacli_buf_inc (& fltr -> buf ,& fltr -> bufsiz ,& fltr -> buflen ,len + 1 )) // additional bytes beyond current contents ( +1 for zero term)
523532 return -1 ; // no memory
524533
525534 memcpy (fltr -> buf + fltr -> buflen ,line ,len );
@@ -598,7 +607,10 @@ static inline void yacli_filter_done_count(filter_inst *fltr) { // {{{
598607} // }}}
599608
600609inline yacli * yacli_init (yascreen * s ) { // {{{
610+ static volatile int verfmt_state = 0 ; // 0=untouched, 1=formatting in progress, 2=done; serializes the one-time reformat of myver
601611 yacli * cli = calloc (1 ,sizeof * cli );
612+ char * rev ;
613+ int vermaj ,vermin ;
602614
603615 if (!cli )
604616 return NULL ;
@@ -612,18 +624,20 @@ inline yacli *yacli_init(yascreen *s) { // {{{
612624 // we use explicit flush, tell yascreen not to do it
613625 yascreen_line_flush (cli -> s ,0 );
614626
615- if (myver [0 ]== 0 ) { // reformat the static version string
616- char * rev = strstr (myver + 1 ,"$Revision: " );
617- int vermaj ,vermin ;
618-
627+ if (__sync_bool_compare_and_swap (& verfmt_state ,0 ,1 )) { // first caller wins, reformats the static version string
628+ rev = strstr (myver + 1 ,"$Revision: " );
619629 if (rev ) {
620630 sscanf (rev + strlen ("$Revision: " ),"%d.%d" ,& vermaj ,& vermin );
621631 vermaj += vermin /100 ;
622632 vermin = vermin %100 ;
623633 memmove (myver ,myver + 1 ,strlen (myver + 1 )+ 1 );
624634 snprintf (rev - 1 ,sizeof myver - (rev - 1 - myver ),"%d.%02d\n\n" ,vermaj ,vermin );
625635 }
626- }
636+ __sync_synchronize ();
637+ verfmt_state = 2 ;
638+ } else
639+ while (verfmt_state != 2 ) // another thread is formatting; wait for it to finish before reading myver
640+ __sync_synchronize ();
627641
628642 cli -> hostname = strdup ("none" );
629643 if (!cli -> hostname )
@@ -759,8 +773,10 @@ static inline void yacli_clear_parsed(yacli *cli) { // {{{
759773 return ;
760774 }
761775
762- for (i = 0 ;i < cli -> parsedcnt ;i ++ )
776+ for (i = 0 ;i < cli -> parsedcnt ;i ++ ) {
763777 free (cli -> parsedcmd [i ]);
778+ cli -> parsedcmd [i ]= NULL ;
779+ }
764780 cli -> parsedcnt = 0 ;
765781} // }}}
766782
@@ -834,6 +850,8 @@ inline void yacli_set_banner(yacli *cli,const char *banner) { // {{{
834850
835851 if (!cli )
836852 return ;
853+ if (!banner )
854+ return ;
837855
838856 t = cli -> banner ;
839857 cli -> banner = strdup (banner );
@@ -848,6 +866,8 @@ inline void yacli_set_level(yacli *cli,const char *level) { // {{{
848866
849867 if (!cli )
850868 return ;
869+ if (!level )
870+ return ;
851871
852872 t = cli -> level ;
853873 cli -> level = strdup (level );
@@ -863,6 +883,8 @@ inline void yacli_set_hostname(yacli *cli,const char *hostname) { // {{{
863883
864884 if (!cli )
865885 return ;
886+ if (!hostname )
887+ return ;
866888
867889 t = cli -> hostname ;
868890 cli -> hostname = strdup (hostname );
@@ -1205,10 +1227,12 @@ inline int yacli_add_hist(yacli *cli,const char *buf) { // {{{
12051227
12061228 if (!cli )
12071229 return 0 ;
1230+ if (!buf )
1231+ return 0 ;
12081232
12091233 cli -> hst_p = NULL ;
12101234
1211- if (!strlen ( buf ) ) // skip empty command
1235+ if (!* buf ) // skip empty command
12121236 return 0 ;
12131237 if (cli -> hst && !strcmp (buf ,cli -> hst -> prev -> command )) // skip repeated command
12141238 return 0 ;
@@ -1233,9 +1257,32 @@ inline int yacli_add_hist(yacli *cli,const char *buf) { // {{{
12331257 cli -> hst -> prev = h ;
12341258 h -> prev -> next = h ;
12351259 }
1260+ cli -> hstcnt ++ ;
1261+ while (cli -> hstmax > 0 && cli -> hstcnt > cli -> hstmax && cli -> hst ) { // evict oldest entries until under cap
1262+ history * t = cli -> hst ; // hst points to the OLDEST item; hst->prev is the newest
1263+
1264+ if (t -> next == t ) { // single item — should not happen if we just added one
1265+ free (t -> command );
1266+ free (t );
1267+ cli -> hst = NULL ;
1268+ } else {
1269+ cli -> hst = t -> next ;
1270+ cli -> hst -> prev = t -> prev ;
1271+ t -> prev -> next = cli -> hst ;
1272+ free (t -> command );
1273+ free (t );
1274+ }
1275+ cli -> hstcnt -- ;
1276+ }
12361277 return 0 ;
12371278} // }}}
12381279
1280+ inline void yacli_set_hist_max (yacli * cli ,int n ) { // {{{
1281+ if (!cli )
1282+ return ;
1283+ cli -> hstmax = n < 0 ?0 :n ;
1284+ } // }}}
1285+
12391286static inline void yacli_moveleft (yacli * cli ) { // {{{
12401287 if (!cli )
12411288 return ;
@@ -1266,6 +1313,8 @@ static inline void yacli_moveleftw(yacli *cli) { // {{{
12661313 if (!cli )
12671314 return ;
12681315
1316+ yacli_buf_zeroterm (cli ); // assure buffer[buflen]=='\0' so walks past valid data terminate cleanly
1317+
12691318 if (cli -> cursor ) {
12701319 if (cli -> buffer [cli -> cursor ]!= ' ' && cli -> buffer [cli -> cursor - 1 ]== ' ' ) // if we are at word begin, step left
12711320 cli -> cursor -- ;
@@ -1287,6 +1336,8 @@ static inline void yacli_moverightw(yacli *cli) { // {{{
12871336 if (!cli )
12881337 return ;
12891338
1339+ yacli_buf_zeroterm (cli ); // assure buffer[buflen]=='\0'
1340+
12901341 if (cli -> cursor < cli -> buflen ) {
12911342 int shiftr = yacli_bufpos_shiftr (cli );
12921343
@@ -1369,6 +1420,8 @@ static inline void yacli_delword(yacli *cli) { // {{{
13691420 if (cli -> cursor >=cli -> buflen ) // nothing to delete
13701421 return ;
13711422
1423+ yacli_buf_zeroterm (cli ); // assure buffer[buflen]=='\0'
1424+
13721425 if (cli -> buffer [cli -> cursor ]== ' ' )
13731426 while (cli -> buffer [cli -> cursor ]== ' ' && cli -> cursor < cli -> buflen )
13741427 yacli_del (cli );
@@ -1383,6 +1436,8 @@ static inline void yacli_delprevword(yacli *cli) { // {{{
13831436 if (!cli -> cursor ) // nothing to delete
13841437 return ;
13851438
1439+ yacli_buf_zeroterm (cli ); // assure buffer[buflen]=='\0'
1440+
13861441 if (cli -> cursor > 0 && cli -> buffer [cli -> cursor - 1 ]== ' ' )
13871442 while (cli -> buffer [cli -> cursor - 1 ]== ' ' && cli -> cursor )
13881443 yacli_bsp (cli );
@@ -1406,14 +1461,17 @@ static inline void yacli_setbuf(yacli *cli,const char *buf) { // {{{
14061461
14071462 if (!cli )
14081463 return ;
1464+ if (!buf )
1465+ return ;
14091466
14101467 len = strlen (buf );
1411- add = len + 1 - cli -> bufsiz ; // add 1 for zero term by strcpy
1468+ add = len + 1 - cli -> buflen ; // additional bytes beyond current valid contents (+1 for trailing null)
14121469
14131470 if (yacli_buf_inc (& cli -> buffer ,& cli -> bufsiz ,& cli -> buflen ,add )) // error in malloc
14141471 return ;
14151472
1416- strncpy (cli -> buffer ,buf ,cli -> bufsiz );
1473+ memcpy (cli -> buffer ,buf ,len );
1474+ cli -> buffer [len ]= 0 ;
14171475 cli -> buflen = len ;
14181476 cli -> cursor = len ;
14191477 cli -> redraw = 1 ;
@@ -1858,11 +1916,12 @@ static inline void yacli_cmd_dump(yacli *cli) { // {{{
18581916static inline void yacli_replace (yacli * cli ,int pos ,int len ,const char * word ) { // {{{
18591917 int wlen = strlen (word );
18601918 int add = wlen - len ;
1919+ int grow = add > 0 ?add + 1 :1 ; // need add extra bytes for the new content + 1 for trailing null safety
18611920
18621921 if (!cli )
18631922 return ;
18641923
1865- if (yacli_buf_inc (& cli -> buffer ,& cli -> bufsiz ,& cli -> buflen ,1 )) // assure buffer can hold what is needed
1924+ if (yacli_buf_inc (& cli -> buffer ,& cli -> bufsiz ,& cli -> buflen ,grow ))
18661925 return ;
18671926 memmove (cli -> buffer + pos + wlen ,cli -> buffer + pos + len ,cli -> buflen - pos - len );
18681927 memcpy (cli -> buffer + pos ,word ,wlen );
@@ -1988,15 +2047,25 @@ static inline int yacli_trycomplete(yacli *cli,int docomplete) { // {{{
19882047 }
19892048
19902049 if (strlen (word )) { // ignore trailing ws yielding empty word
2050+ cmnode * dyn_resume = NULL ; // sibling after a @-node we just stepped into; revisit if nothing in the dyn list matches
2051+
19912052 for (;;) {
19922053 int cmp ;
19932054
19942055 if (cn -> cmd [0 ]== '@' ) { // dynamic command
19952056 yacli_dyn_upd (cli ,cn ,word );
19962057 dyn = 1 ;
2058+ dyn_resume = cn -> next ; // any literal/^ sibling sitting after the @ — try it if dyn list yields nothing
19972059 cn = cn -> dyn ;
1998- if (!cn )
2060+ if (!cn ) { // empty dyn list; resume from the saved sibling, if any
2061+ if (dyn_resume ) {
2062+ cn = dyn_resume ;
2063+ dyn_resume = NULL ;
2064+ lastcn = cn ;
2065+ continue ;
2066+ }
19992067 break ;
2068+ }
20002069 cmp = strcmp (word ,cn -> cmd );
20012070 } else if (cn -> cmd [0 ]== '^' ) { // regex
20022071 cmp = yacli_regx (cn -> cmd ,word );
@@ -2051,6 +2120,12 @@ static inline int yacli_trycomplete(yacli *cli,int docomplete) { // {{{
20512120 lastcn = cn ;
20522121 continue ;
20532122 }
2123+ if (dyn_resume ) { // exhausted dyn list without a match; retry the saved sibling chain
2124+ cn = dyn_resume ;
2125+ dyn_resume = NULL ;
2126+ lastcn = cn ;
2127+ continue ;
2128+ }
20542129 yacli_print_nof (cli ,"\nNo matched command (2)\n" );
20552130 cli -> redraw = 1 ;
20562131 free (fb );
@@ -2161,6 +2236,10 @@ static inline int yacli_trycomplete(yacli *cli,int docomplete) { // {{{
21612236 break ;
21622237 } else { // regex match always returns 1 on no match
21632238 cn = cn -> next ;
2239+ if (!cn && dyn_resume ) { // exhausted dyn list (or chain past @) without matching; retry from saved sibling
2240+ cn = dyn_resume ;
2241+ dyn_resume = NULL ;
2242+ }
21642243 if (cn )
21652244 lastcn = cn ;
21662245 if (!cn ) {
@@ -2231,7 +2310,7 @@ static inline int yacli_trycomplete(yacli *cli,int docomplete) { // {{{
22312310 havenextfltr = 1 ;
22322311 * worde = 0 ;
22332312 }
2234- while (worde [-1 ]== ' ' ) { // sooner or later we will hit \0 or |
2313+ while (worde > word && worde [-1 ]== ' ' ) { // strip trailing ws of filter args; bounded by start of args
22352314 worde [-1 ]= 0 ;
22362315 worde -- ;
22372316 }
@@ -2957,6 +3036,14 @@ inline void *yacli_add_cmd(yacli *cli,void *parent,const char *cmd,const char *h
29573036 if (par && par -> cli != cli ) // parent node must match cli
29583037 return NULL ;
29593038
3039+ if (cmd [0 ]== '^' ) { // validate the regex up-front; reject malformed patterns now rather than silently failing every match later
3040+ regex_t re ;
3041+
3042+ if (regcomp (& re ,cmd ,REG_EXTENDED )!= 0 )
3043+ return NULL ;
3044+ regfree (& re );
3045+ }
3046+
29603047 if (par ) // find proper place in the tree
29613048 place = & par -> child ;
29623049 else
@@ -3035,7 +3122,7 @@ inline void yacli_list(yacli *cli,void *ctx,const char *item) { // {{{
30353122
30363123static inline void yacli_gen_modes (yacli * cli ) { // {{{
30373124 int modelen = 0 ;
3038- cmstack * s ,* l ;
3125+ cmstack * s ,* l = NULL ;
30393126 int first = 1 ;
30403127
30413128 if (!cli )
@@ -3078,6 +3165,8 @@ inline void yacli_enter_mode(yacli *cli,const char *mode,void *hint) { // {{{
30783165
30793166 if (!cli )
30803167 return ;
3168+ if (!mode )
3169+ return ;
30813170
30823171 s = calloc (1 ,sizeof * s );
30833172 if (!s ) // crit err - no memory
@@ -3087,6 +3176,12 @@ inline void yacli_enter_mode(yacli *cli,const char *mode,void *hint) { // {{{
30873176 if (s -> next )
30883177 s -> next -> prev = s ;
30893178 s -> mode = strdup (mode );
3179+ if (!s -> mode ) { // alloc failure: undo the partial push
3180+ if (s -> next )
3181+ s -> next -> prev = NULL ;
3182+ free (s );
3183+ return ;
3184+ }
30903185 s -> cmdt = cli -> cmdt ;
30913186 s -> hint = hint ;
30923187 cli -> cmdt = NULL ;
0 commit comments