xref: /freebsd/usr.bin/ident/ident.c (revision a0ee8cc6)
1 /*-
2  * Copyright (c) 2015 Baptiste Daroussin <bapt@FreeBSD.org>
3  * Copyright (c) 2015 Xin LI <delphij@FreeBSD.org>
4  * All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  * 1. Redistributions of source code must retain the above copyright
10  *    notice, this list of conditions and the following disclaimer
11  *    in this position and unchanged.
12  * 2. Redistributions in binary form must reproduce the above copyright
13  *    notice, this list of conditions and the following disclaimer in the
14  *    documentation and/or other materials provided with the distribution.
15  *
16  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
17  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
18  * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
19  * IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
20  * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
21  * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
25  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26  */
27 
28 #include <sys/cdefs.h>
29 __FBSDID("$FreeBSD$");
30 
31 #include <sys/types.h>
32 #include <sys/sbuf.h>
33 
34 #include <ctype.h>
35 #include <err.h>
36 #include <stdbool.h>
37 #include <stdio.h>
38 #include <stdlib.h>
39 #include <unistd.h>
40 #include <xlocale.h>
41 
42 typedef enum {
43 	/* state	condition to transit to next state */
44 	INIT,		/* '$' */
45 	DELIM_SEEN,	/* letter */
46 	KEYWORD,	/* punctuation mark */
47 	PUNC_SEEN,	/* ':' -> _SVN; space -> TEXT */
48 	PUNC_SEEN_SVN,	/* space */
49 	TEXT
50 } analyzer_states;
51 
52 static int
53 scan(FILE *fp, const char *name, bool quiet)
54 {
55 	int c;
56 	bool hasid = false;
57 	bool subversion = false;
58 	analyzer_states state = INIT;
59 	struct sbuf *id = sbuf_new_auto();
60 	locale_t l;
61 
62 	l = newlocale(LC_ALL_MASK, "C", NULL);
63 
64 	if (name != NULL)
65 		printf("%s:\n", name);
66 
67 	while ((c = fgetc(fp)) != EOF) {
68 		switch (state) {
69 		case INIT:
70 			if (c == '$') {
71 				/* Transit to DELIM_SEEN if we see $ */
72 				state = DELIM_SEEN;
73 			} else {
74 				/* Otherwise, stay in INIT state */
75 				continue;
76 			}
77 			break;
78 		case DELIM_SEEN:
79 			if (isalpha_l(c, l)) {
80 				/* Transit to KEYWORD if we see letter */
81 				sbuf_clear(id);
82 				sbuf_putc(id, '$');
83 				sbuf_putc(id, c);
84 				state = KEYWORD;
85 
86 				continue;
87 			} else if (c == '$') {
88 				/* Or, stay in DELIM_SEEN if more $ */
89 				continue;
90 			} else {
91 				/* Otherwise, transit back to INIT */
92 				state = INIT;
93 			}
94 			break;
95 		case KEYWORD:
96 			sbuf_putc(id, c);
97 
98 			if (isalpha_l(c, l)) {
99 				/*
100 				 * Stay in KEYWORD if additional letter is seen
101 				 */
102 				continue;
103 			} else if (c == ':') {
104 				/*
105 				 * See ':' for the first time, transit to
106 				 * PUNC_SEEN.
107 				 */
108 				state = PUNC_SEEN;
109 				subversion = false;
110 			} else if (c == '$') {
111 				/*
112 				 * Incomplete ident.  Go back to DELIM_SEEN
113 				 * state because we see a '$' which could be
114 				 * the beginning of a keyword.
115 				 */
116 				state = DELIM_SEEN;
117 			} else {
118 				/*
119 				 * Go back to INIT state otherwise.
120 				 */
121 				state = INIT;
122 			}
123 			break;
124 		case PUNC_SEEN:
125 		case PUNC_SEEN_SVN:
126 			sbuf_putc(id, c);
127 
128 			switch (c) {
129 			case ':':
130 				/*
131 				 * If we see '::' (seen : in PUNC_SEEN),
132 				 * activate subversion treatment and transit
133 				 * to PUNC_SEEN_SVN state.
134 				 *
135 				 * If more than two :'s were seen, the ident
136 				 * is invalid and we would therefore go back
137 				 * to INIT state.
138 				 */
139 				if (state == PUNC_SEEN) {
140 					state = PUNC_SEEN_SVN;
141 					subversion = true;
142 				} else {
143 					state = INIT;
144 				}
145 				break;
146 			case ' ':
147 				/*
148 				 * A space after ':' or '::' indicates we are at the
149 				 * last component of potential ident.
150 				 */
151 				state = TEXT;
152 				break;
153 			default:
154 				/* All other characters are invalid */
155 				state = INIT;
156 				break;
157 			}
158 			break;
159 		case TEXT:
160 			sbuf_putc(id, c);
161 
162 			if (iscntrl_l(c, l)) {
163 				/* Control characters are not allowed in this state */
164 				state = INIT;
165 			} else if (c == '$') {
166 				sbuf_finish(id);
167 				/*
168 				 * valid ident should end with a space.
169 				 *
170 				 * subversion extension uses '#' to indicate that
171 				 * the keyword expansion have exceeded the fixed
172 				 * width, so it is also permitted if we are in
173 				 * subversion mode.  No length check is enforced
174 				 * because GNU RCS ident(1) does not do it either.
175 				 */
176 				c = sbuf_data(id)[sbuf_len(id) - 2];
177 				if (c == ' ' || (subversion && c == '#')) {
178 					printf("     %s\n", sbuf_data(id));
179 					hasid = true;
180 				}
181 				state = INIT;
182 			}
183 			/* Other characters: stay in the state */
184 			break;
185 		}
186 	}
187 	sbuf_delete(id);
188 	freelocale(l);
189 
190 	if (!hasid) {
191 		if (!quiet)
192 			fprintf(stderr, "%s warning: no id keywords in %s\n",
193 			    getprogname(), name ? name : "standard input");
194 
195 		return (EXIT_FAILURE);
196 	}
197 
198 	return (EXIT_SUCCESS);
199 }
200 
201 int
202 main(int argc, char **argv)
203 {
204 	bool quiet = false;
205 	int ch, i;
206 	int ret = EXIT_SUCCESS;
207 	FILE *fp;
208 
209 	while ((ch = getopt(argc, argv, "qV")) != -1) {
210 		switch (ch) {
211 		case 'q':
212 			quiet = true;
213 			break;
214 		case 'V':
215 			/* Do nothing, compat with GNU rcs's ident */
216 			return (EXIT_SUCCESS);
217 		default:
218 			errx(EXIT_FAILURE, "usage: %s [-q] [-V] [file...]",
219 			    getprogname());
220 		}
221 	}
222 
223 	argc -= optind;
224 	argv += optind;
225 
226 	if (argc == 0)
227 		return (scan(stdin, NULL, quiet));
228 
229 	for (i = 0; i < argc; i++) {
230 		fp = fopen(argv[i], "r");
231 		if (fp == NULL) {
232 			warn("%s", argv[i]);
233 			ret = EXIT_FAILURE;
234 			continue;
235 		}
236 		if (scan(fp, argv[i], quiet) != EXIT_SUCCESS)
237 			ret = EXIT_FAILURE;
238 		fclose(fp);
239 	}
240 
241 	return (ret);
242 }
243