├── LICENSE ├── Makefile ├── README ├── plumb.1 └── plumb.c /LICENSE: -------------------------------------------------------------------------------- 1 | MIT/X Consortium License 2 | 3 | © 2022-2023 Lucas de Sena 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROG = plumb 2 | OBJS = ${PROG:=.o} 3 | SRCS = ${OBJS:.o=.c} 4 | MAN = ${PROG:=.1} 5 | 6 | PREFIX ?= /usr/local 7 | MANPREFIX ?= ${PREFIX}/share/man 8 | 9 | DEFS = -D_POSIX_C_SOURCE=200809L -DGNU_SOURCE -D_BSD_SOURCE 10 | LIBS = -L/usr/local/lib -lmagic 11 | INCS = -I/usr/local/include 12 | 13 | bindir = ${DESTDIR}${PREFIX}/bin 14 | mandir = ${DESTDIR}${MANPREFIX}/man1 15 | 16 | all: ${PROG} 17 | 18 | ${PROG}: ${OBJS} 19 | ${CC} -o $@ ${OBJS} ${LIBS} ${LDFLAGS} 20 | 21 | .c.o: 22 | ${CC} -std=c99 -pedantic ${DEFS} ${INCS} ${CFLAGS} ${CPPFLAGS} -o $@ -c $< 23 | 24 | README: ${MAN} 25 | mandoc -I os=UNIX -T ascii ${MAN} | col -b | expand -t 8 >README 26 | 27 | tags: ${SRCS} 28 | ctags ${SRCS} 29 | 30 | lint: ${SRCS} 31 | -mandoc -T lint -W warning ${MAN} 32 | -clang-tidy ${SRCS} -- -std=c99 ${DEFS} ${INCS} ${CPPFLAGS} 33 | 34 | clean: 35 | rm -f ${OBJS} ${PROG} ${PROG:=.core} tags 36 | 37 | install: all 38 | mkdir -p ${bindir} 39 | mkdir -p ${mandir} 40 | install -m 755 ${PROG} ${bindir}/${PROG} 41 | install -m 644 ${MAN} ${mandir}/${MAN} 42 | 43 | uninstall: 44 | -rm ${bindir}/${PROG} 45 | -rm ${mandir}/${MAN} 46 | 47 | .PHONY: all tags clean install uninstall lint 48 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | PROG(1) General Commands Manual PROG(1) 2 | 3 | NAME 4 | plumb - run best command for given arguments 5 | 6 | SYNOPSIS 7 | plumb [-action ...] [--] arg ... 8 | 9 | DESCRIPTION 10 | plumb passes the given arguments, as is or modified, to the best command 11 | associated to the rules the arguments match with. 12 | 13 | plumb can be used, for example, to open files or URLs. By giving 14 | filenames as arguments, plumb tries to find the best command to open them 15 | after matching the filenames with sets of rules in sequence. See the 16 | section EXAMPLES for illustration. 17 | 18 | Each set of rule (or ruleset) is associated with a command for a given 19 | type of action (such as "open" or "edit"). The command for the given 20 | action associated with the first ruleset matching the arguments is the 21 | one chosen to be run. 22 | 23 | The first arguments beginning with hyphen (-), are interpreted as a 24 | actions to be try. If the matching ruleset contains one of those 25 | actions, the first action is performed. For example, -edit -open will 26 | try to perform either the edit or the open action on the following 27 | arguments. 28 | 29 | The actions -o and -e are equivalent to -open and -edit, respectively. 30 | 31 | A -- separates actions from actual arguments. 32 | 33 | If no action is provided, plumb acts as if the action -open were given. 34 | 35 | USAGE 36 | plumb reads rules from the file $HOME/lib/plumb. Lines with blank and 37 | lines beginning with "# are ignored." 38 | 39 | Each non ignored line can be either a static variable assignment or can 40 | be one of the four following types, named after the second word in the 41 | line: 42 | 43 | "for" A line beginning a ruleset. 44 | 45 | "matches" 46 | A line describing a condition a variable has to match case- 47 | sensitively, and, optionally, setting new variables when the 48 | matching occurs. 49 | 50 | "imatches" 51 | A line describing a condition a variable has to match case- 52 | insensitively, and, optionally, setting new variables when the 53 | matching occurs. 54 | 55 | "types" 56 | A line testing the existence and type of a file, and assigning 57 | this information to a new variable. 58 | 59 | "with" A line describing the command to be open if the conditions in the 60 | same ruleset matches. 61 | 62 | There are two kinds of variables that can be assigned and used in the 63 | configuration file: 64 | 65 | Static variables 66 | Static variables are assigned with a "NAME=VALUE" line. Such 67 | variables are expanded into a single word when prefixed with a 68 | dollar sign ($) and occurring in any line after the place it was 69 | defined. Environment variables are of this kind, but do not need 70 | to be defined. See the section Static variables for more 71 | information on static variables. 72 | 73 | Argument variables 74 | Argument variables are assigned with a "matches", "imatches", or 75 | a "types" line. Such variables can be expanded into various 76 | words (one for each argument passed to plumb) when prefixed with 77 | a percent sign (%) and occurring as the last argument of a "with" 78 | line in the same ruleset it was defined. See the section WITH- 79 | lines for more information on argument variables. 80 | 81 | Each line is a sequence of words (which are either space-delimited words 82 | or strings quoted in rc(1) single-quote style). The first word of a line 83 | is the "subject". The second word (which identifies the type of the 84 | line) is the "verb". The remaining words are the arguments. 85 | 86 | The configuration is processed once for each argument. For each 87 | processing pass, the argument variable data is set to the argument itself 88 | and the remaining argument variables are re-assigned. Static variables 89 | are assigned only once, at plumb initialization. 90 | 91 | Static variables 92 | Lines of the form "NAME=VALUE" assign a value to a static variable. 93 | Static variable are recognized anywhere in the file after the place they 94 | are defined. 95 | 96 | Environment variables are also static variables, but they are not defined 97 | in the config file (they are already defined in the environment). 98 | 99 | References to static variables can occur on the configuration file 100 | outside quotations, and are replaced with their values. Such references 101 | are prefixed with the dollar sign "$" or prefixed with the dollar sign 102 | and surrounded by curly braces. The dollar sign can be escaped by 103 | doubling it. 104 | 105 | FOR-lines 106 | Lines whose second word is "for" must have "rules" as subject. They 107 | begin a new ruleset. A ruleset is everything between a "FOR-line" and 108 | the next one. 109 | 110 | For example, the following line begins the ruleset for handling video 111 | files: 112 | 113 | rules for video files 114 | 115 | The arguments of a "FOR-line" are the name of the ruleset. 116 | 117 | Conditions in a ruleset are only checked within the ruleset. Variables 118 | set in a ruleset are only valid within the ruleset. 119 | 120 | The lines before the first "FOR-line" make the global, unamed ruleset. 121 | Conditions in the global ruleset are ignored. Variables set in the 122 | global ruleset are valid for the entire file. 123 | 124 | MATCHES-lines 125 | Lines whose second word is "matches" or "imatches" must have the name of 126 | a variable as subject (first word in the line). They must also have a 127 | regular expression as first argument. The subject names a value that 128 | must match the regular expression. 129 | 130 | If the second word is "matches", the regular expression matching is case- 131 | sensitive. If the second word is "imatches", the regular expression 132 | matching is case-insensitive. 133 | 134 | For example, the following line is a three-word condition that says that 135 | one of the conditions for the current ruleset to be matched is for the 136 | content of the variable mime to match the regular expression 137 | image/(jpeg|png). 138 | 139 | mime matches 'image/(jpeg|png)' 140 | 141 | The regular expression is a extended POSIX regular expression and must 142 | match the entire value of the argument variable for the condition to be 143 | valid. 144 | 145 | If the rule has more than one argument, the second argument must be into 146 | and the following ones must be names of argument variables to be set. 147 | Each argument variable is set to the substring matching the parenthesized 148 | subexpression of the regular expression if, and only if, the full regular 149 | expression matches the value of the subject. 150 | 151 | For example, the following line assigns to the argument variable base the 152 | basename(1) of the value on the argument variable data (supposing it 153 | contains a filename); and assigns to the argument variable extension the 154 | extension of the filename. If either subexpression does not match, the 155 | corresponding argument variable is set to the empty string. The dummy 156 | argument variable name _ (underscore) is used for uneeded values. 157 | 158 | data matches '(([^/]*/)*)([^/]*(\.([A-Za-z0-9]+)?))' _ _ base extension 159 | 160 | TYPES-lines 161 | Lines whose second word is "types" must have the name of a argument 162 | variable as subject, and the name of another argument variable as single 163 | argument. The subject names a value for a existing file whose mimetype 164 | is assigned to the argument variable passed as argument. 165 | 166 | For example, the following line is a three-word assignment that says that 167 | the mimetype of the file named in the argument variable data must be 168 | assigned to the argument variable mime. 169 | 170 | data types mime 171 | 172 | WITH-lines 173 | Lines whose second word is "with" must have the name of an action type 174 | (like open or edit) as subject and a command invocation as arguments. 175 | The arguments name a program to be run for the action named as subject 176 | when the ruleset the line is in is valid for all the arguments passed. 177 | 178 | for example, the following line is a three-word description to open the 179 | browser firefox(1) on the open action. 180 | 181 | open with firefox 182 | 183 | If the last argument has a percent symbol ("%") before a name, then this 184 | name is considered as a variable name. This argument is replaced by one 185 | argument for each argument passed and the variable name with the percent 186 | sign is replaced with the value of the variable. 187 | 188 | For example, the following line opens firefox(1) replacing the argument 189 | file://%data for the variable data for each argument. (so if plumb is 190 | invoked for ./index.html and /path/to/file.html, then that single 191 | argument is replaced with file://./index.html and 192 | file:///path/to/file.html). 193 | 194 | open with firefox -- file://%data 195 | 196 | Just like environment variables, the percent sign can be escaped by 197 | doubling it. The name of the variable can also occur between curly 198 | braces. 199 | 200 | ENVIRONMENT 201 | The following environment variables affect the execution of plumb. 202 | 203 | HOME Path to the directory to search for the file lib/plumb. 204 | 205 | FILES 206 | $HOME/lib/plumb 207 | plumb's default configuration file. 208 | 209 | EXIT STATUS 210 | The plumb utility exits 0 on success, and >0 if an error occurs. 211 | 212 | It is an error if no ruleset matches for an argument. 213 | 214 | EXAMPLES 215 | The following is the example of a simple configuration file. 216 | 217 | DATAREGEX = '(([A-Za-z]+):(//)?)?(.*(\.([A-Za-z0-9]+))?)' 218 | 219 | data matches $DATAREGEX into _ protocol _ file _ extension 220 | file types mime 221 | 222 | rules for youtube video 223 | protocol matches '(ytdl|https?)?' 224 | file matches '(.*/)?[A-Za-z0-9_-]{11}' 225 | open with mpv --force-window=immediate -- %data 226 | 227 | rules for image file 228 | protocol matches '(file)?' 229 | mime imatches 'image/(png|jpe?g|tiff)' 230 | open with display -- %file 231 | edit with gimp -- %file 232 | 233 | rules for web page 234 | protocol matches '(https?|file)?' 235 | extension imatches 'html' 236 | open with seamonkey -- %data 237 | 238 | This configuration file is interpreted as follows: 239 | 240 | o The static variable DATAREGEX is set to a regular expression used 241 | later in the config file. 242 | 243 | o For each passed argument, the second paragraph sets the argument 244 | variables "protocol" to an URI protocol; "file" to the argument 245 | without the protocol; "extension" to a file extension; and "mime" to 246 | the mimetype of the value of "file". The argument variable "data" is 247 | automatically set to the argument itself on each pass. 248 | 249 | o The third paragraph sets rules for opening youtube videos on mpv(1) 250 | using the ytdl protocol. 251 | 252 | o The fourth paragraph sets rules for opening and editing image files. 253 | 254 | o The fifth paragraph sets rules for opening web pages. 255 | 256 | With this configuration file, the following command opens 257 | https://wikipedia.org and file:///var/www/htdocs/index.html on 258 | seamonkey(1): 259 | 260 | $ plumb https://wikipedia.org file:///var/www/htdocs/index.html 261 | 262 | The following command opens a PNG file on gimp for editing: 263 | 264 | $ plumb -edit /home/user/photo.png 265 | 266 | SEE ALSO 267 | Rob Pike, Plumbing and Other Utilities, Bell Laboratories. 268 | 269 | HISTORY 270 | A plumb utility appeared in the Plan 9 operating system. 271 | 272 | UNIX July 2, 2023 UNIX 273 | -------------------------------------------------------------------------------- /plumb.1: -------------------------------------------------------------------------------- 1 | .Dd July 2, 2023 2 | .Dt PLUMB 1 3 | .Os 4 | .Sh NAME 5 | .Nm plumb 6 | .Nd run best command for given arguments 7 | .Sh SYNOPSIS 8 | .Nm 9 | .Op Fl Ar action ... 10 | .Op Cm "--" 11 | .Ar arg ... 12 | .Sh DESCRIPTION 13 | .Nm 14 | passes the given arguments, as is or modified, 15 | to the best command associated to the rules the arguments match with. 16 | .Pp 17 | .Nm 18 | can be used, for example, to open files or URLs. 19 | By giving filenames as arguments, 20 | .Nm 21 | tries to find the best command to open them 22 | after matching the filenames with sets of rules in sequence. 23 | See the section 24 | .Sx "EXAMPLES" 25 | for illustration. 26 | .Pp 27 | Each set of rule (or ruleset) is associated with a command for a given type of action 28 | (such as 29 | .Qq "open" 30 | or 31 | .Qq "edit" ) . 32 | The command for the given action associated with the first ruleset 33 | matching the arguments is the one chosen to be run. 34 | .Pp 35 | The first arguments beginning with hyphen 36 | .Pq Cm "-" , 37 | are interpreted as a actions to be try. 38 | If the matching ruleset contains one of those actions, 39 | the first action is performed. 40 | For example, 41 | .Fl edit Fl open 42 | will try to perform either the edit or the open action on the following arguments. 43 | .Pp 44 | The actions 45 | .Fl o 46 | and 47 | .Fl e 48 | are equivalent to 49 | .Fl open 50 | and 51 | .Fl edit , 52 | respectively. 53 | .Pp 54 | A 55 | .Cm "--" 56 | separates actions from actual arguments. 57 | .Pp 58 | If no action is provided, 59 | .Nm 60 | acts as if the action 61 | .Cm "-open" 62 | were given. 63 | .Sh USAGE 64 | .Nm 65 | reads rules from the file 66 | .Pa "$HOME/lib/plumb" . 67 | Lines with blank and lines beginning with 68 | .Qq "#" are ignored. 69 | .Pp 70 | Each non ignored line can be either a static variable assignment 71 | or can be one of the five following types, 72 | named after the second word in the line: 73 | .Bl -tag -width Ds 74 | .It Qq "for" 75 | A line beginning a ruleset. 76 | .It Qq "matches" 77 | A line describing a condition a variable has to match case-sensitively, 78 | and, optionally, setting new variables when the matching occurs. 79 | .It Qq "imatches" 80 | A line describing a condition a variable has to match case-insensitively, 81 | and, optionally, setting new variables when the matching occurs. 82 | .It Qq "types" 83 | A line testing the existence and type of a file, 84 | and assigning this information to a new variable. 85 | .It Qq "at" 86 | A line testing the existence of a file, 87 | and assigning its absolute path to a new variable. 88 | .It Qq "with" 89 | A line describing the command to be open if the conditions in the same ruleset matches. 90 | .El 91 | .Pp 92 | There are two kinds of variables that can be assigned and used in the configuration file: 93 | .Bl -tag -width Ds 94 | .It Static variables 95 | Static variables are assigned with a 96 | .Qq "NAME=VALUE" 97 | line. 98 | Such variables are expanded into a single word 99 | when prefixed with a dollar sign 100 | .Pq "$" 101 | and occurring in any line after the place it was defined. 102 | Environment variables are of this kind, but do not need to be defined. 103 | See the section 104 | .Sx "Static variables" 105 | for more information on static variables. 106 | .It Argument variables 107 | Argument variables are assigned with a 108 | .Qq "matches" , 109 | .Qq "imatches" , 110 | .Qq "types" , 111 | or a 112 | .Qq "at" 113 | line. 114 | Such variables can be expanded into various words 115 | (one for each argument passed to 116 | .Nm ) 117 | when prefixed with a percent sign 118 | .Pq "%" 119 | and occurring as the last argument of a 120 | .Qq "with" 121 | line in the same ruleset it was defined. 122 | See the section 123 | .Sx "WITH-lines" 124 | for more information on argument variables. 125 | .El 126 | .Pp 127 | Each line is a sequence of words 128 | (which are either space-delimited words or 129 | strings quoted in 130 | .Xr rc 1 131 | single-quote style). 132 | The first word of a line is the 133 | .Qq "subject" . 134 | The second word (which identifies the type of the line) is the 135 | .Qq "verb" . 136 | The remaining words are the arguments. 137 | .Pp 138 | The configuration is processed once for each argument. 139 | For each processing pass, the argument variable 140 | .Ic data 141 | is set to the argument itself 142 | and the remaining argument variables are re-assigned. 143 | Static variables are assigned only once, at 144 | .Nm 145 | initialization. 146 | .Ss Static variables 147 | Lines of the form 148 | .Qq "NAME=VALUE" 149 | assign a value to a static variable. 150 | Static variable are recognized anywhere in the file after the place they are defined. 151 | .Pp 152 | Environment variables are also static variables, but they are not defined in the config file 153 | (they are already defined in the environment). 154 | .Pp 155 | References to static variables can occur on the configuration file outside quotations, 156 | and are replaced with their values. 157 | Such references are prefixed with the dollar sign 158 | .Qq "$" 159 | or prefixed with the dollar sign and surrounded by curly braces. 160 | The dollar sign can be escaped by doubling it. 161 | .Ss FOR-lines 162 | Lines whose second word is 163 | .Qq "for" 164 | must have 165 | .Qq "rules" 166 | as subject. 167 | They begin a new ruleset. 168 | A ruleset is everything between a 169 | .Qq FOR-line 170 | and the next one. 171 | .Pp 172 | For example, the following line begins the ruleset for handling video files: 173 | .Bd -literal -offset indent 174 | rules for video files 175 | .Ed 176 | .Pp 177 | The arguments of a 178 | .Qq FOR-line 179 | are the name of the ruleset. 180 | .Pp 181 | Conditions in a ruleset are only checked within the ruleset. 182 | Variables set in a ruleset are only valid within the ruleset. 183 | .Pp 184 | The lines before the first 185 | .Qq FOR-line 186 | make the global, unamed ruleset. 187 | Conditions in the global ruleset are ignored. 188 | Variables set in the global ruleset are valid for the entire file. 189 | .Ss MATCHES-lines 190 | Lines whose second word is 191 | .Qq "matches" 192 | or 193 | .Qq "imatches" 194 | must have the name of a variable as subject (first word in the line). 195 | They must also have a regular expression as first argument. 196 | The subject names a value that must match the regular expression. 197 | .Pp 198 | If the second word is 199 | .Qq "matches" , 200 | the regular expression matching is case-sensitive. 201 | If the second word is 202 | .Qq "imatches" , 203 | the regular expression matching is case-insensitive. 204 | .Pp 205 | For example, the following line is a three-word condition that 206 | says that one of the conditions for the current ruleset to be matched 207 | is for the content of the variable 208 | .Ic mime 209 | to match the regular expression 210 | .Ic "image/(jpeg|png)" . 211 | .Bd -literal -offset indent 212 | mime matches 'image/(jpeg|png)' 213 | .Ed 214 | .Pp 215 | The regular expression is a extended POSIX regular expression 216 | and must match the entire value of the argument variable for the condition to be valid. 217 | .Pp 218 | If the rule has more than one argument, the second argument must be 219 | .Ic into 220 | and the following ones must be names of argument variables to be set. 221 | Each argument variable is set to the substring matching the parenthesized subexpression 222 | of the regular expression if, and only if, the full regular expression matches 223 | the value of the subject. 224 | .Pp 225 | For example, the following line assigns to the argument variable 226 | .Ic base 227 | the 228 | .Xr basename 1 229 | of the value on the argument variable 230 | .Ic data 231 | (supposing it contains a filename); 232 | and assigns to the argument variable 233 | .Ic extension 234 | the extension of the filename. 235 | If either subexpression does not match, the corresponding argument variable is 236 | set to the empty string. 237 | The dummy argument variable name 238 | .Ic _ 239 | (underscore) is used for uneeded values. 240 | .Bd -literal -offset indent 241 | data matches '(([^/]*/)*)([^/]*(\e.([A-Za-z0-9]+)?))' _ _ base extension 242 | .Ed 243 | .Ss TYPES-lines 244 | Lines whose second word is 245 | .Qq "types" 246 | must have the name of an argument variable as subject, 247 | and the name of another argument variable as its sole argument. 248 | The subject names a value for an existing file whose mimetype is assigned 249 | to the argument variable passed as argument. 250 | .Pp 251 | For example, the following line is a three-word assignment that says 252 | that the mimetype of the file named in the argument variable 253 | .Ic "data" 254 | must be assigned to the argument variable 255 | .Ic "mime" . 256 | .Bd -literal -offset indent 257 | data types mime 258 | .Ed 259 | .Ss AT-lines 260 | Lines whose second word is 261 | .Qq "at" 262 | must have the name of an argument variable as subject, 263 | and the name of another argument variable as its sole argument. 264 | The subject names a value for an existing file whose real absolute path is assigned 265 | to the argument variable passed as argument. 266 | .Pp 267 | For example, the following line is a three-word assignment that says 268 | that the absolute path of the file named in the argument variable 269 | .Ic "data" 270 | must be assigned to the argument variable 271 | .Ic "path" . 272 | .Bd -literal -offset indent 273 | data at path 274 | .Ed 275 | .Ss WITH-lines 276 | Lines whose second word is 277 | .Qq "with" 278 | must have the name of an action type 279 | (like 280 | .Ic "open" 281 | or 282 | .Ic "edit" ) 283 | as subject and a command invocation as arguments. 284 | The arguments name a program to be run for the action named as subject 285 | when the ruleset the line is in is valid for all the arguments passed. 286 | .Pp 287 | for example, the following line is a three-word description to open the browser 288 | .Xr firefox 1 289 | on the 290 | .Ic open 291 | action. 292 | .Bd -literal -offset indent 293 | open with firefox 294 | .Ed 295 | .Pp 296 | If the last argument has a percent symbol 297 | .Pq Qq "%" 298 | before a name, 299 | then this name is considered as a variable name. 300 | This argument is replaced by one argument for each argument passed 301 | and the variable name with the percent sign is replaced with the value of the variable. 302 | .Pp 303 | For example, the following line opens 304 | .Xr firefox 1 305 | replacing the argument 306 | .Ic "file://%data" 307 | for the variable 308 | .Ic "data" 309 | for each argument. 310 | (so if 311 | .Nm 312 | is invoked for 313 | .Pa "./index.html" 314 | and 315 | .Pa "/path/to/file.html" , 316 | then that single argument is replaced with 317 | .Pa "file://./index.html" 318 | and 319 | .Pa "file:///path/to/file.html" ) . 320 | .Bd -literal -offset indent 321 | open with firefox -- file://%data 322 | .Ed 323 | .Pp 324 | Just like environment variables, the percent sign can be escaped by doubling it. 325 | The name of the variable can also occur between curly braces. 326 | .Sh ENVIRONMENT 327 | The following environment variables affect the execution of 328 | .Nm . 329 | .Bl -tag -width Ds 330 | .It Ev HOME 331 | Path to the directory to search for the file 332 | .Pa "lib/plumb" . 333 | .El 334 | .Sh FILES 335 | .Bl -tag -width Ds 336 | .It Pa "$HOME/lib/plumb" 337 | .Nm Ns 's 338 | default configuration file. 339 | .El 340 | .Sh EXIT STATUS 341 | .Ex -std 342 | .Pp 343 | It is an error if no ruleset matches for an argument. 344 | .Sh EXAMPLES 345 | The following is the example of a simple configuration file. 346 | .Bd -literal -offset indent 347 | DATAREGEX = '(([A-Za-z]+):(//)?)?(.*(\e.([A-Za-z0-9]+))?)' 348 | 349 | data matches $DATAREGEX into _ protocol _ file _ extension 350 | file types mime 351 | file at path 352 | 353 | rules for youtube video 354 | protocol matches '(ytdl|https?)?' 355 | file matches '(.*/)?[A-Za-z0-9_-]{11}' 356 | open with mpv --force-window=immediate -- %data 357 | 358 | rules for image file 359 | protocol matches '(file)?' 360 | mime imatches 'image/(png|jpe?g|tiff)' 361 | open with display -- %path 362 | edit with gimp -- %file 363 | 364 | rules for web page 365 | protocol matches '(https?|file)?' 366 | extension imatches 'html' 367 | open with seamonkey -- %data 368 | .Ed 369 | .Pp 370 | This configuration file is interpreted as follows: 371 | .Bl -bullet 372 | .It 373 | The static variable 374 | .Ic DATAREGEX 375 | is set to a regular expression used later in the config file. 376 | .It 377 | For each passed argument, the second paragraph sets the argument variables 378 | .Qq Ic protocol 379 | to an URI protocol; 380 | .Qq Ic file 381 | to the argument without the protocol; 382 | .Qq Ic extension 383 | to a file extension; and 384 | .Qq Ic mime 385 | to the mimetype of the value of 386 | .Qq Ic file . 387 | The argument variable 388 | .Qq Ic data 389 | is automatically set to the argument itself on each pass. 390 | .It 391 | The third paragraph sets rules for opening youtube videos on 392 | .Xr mpv 1 393 | using the 394 | .Ic ytdl 395 | protocol. 396 | .It 397 | The fourth paragraph sets rules for opening and editing image files. 398 | .It 399 | The fifth paragraph sets rules for opening web pages. 400 | .El 401 | .Pp 402 | With this configuration file, the following command opens 403 | .Em https://wikipedia.org 404 | and 405 | .Em file:///var/www/htdocs/index.html 406 | on 407 | .Xr seamonkey 1 : 408 | .Bd -literal -offset indent 409 | $ plumb https://wikipedia.org file:///var/www/htdocs/index.html 410 | .Ed 411 | .Pp 412 | The following command opens a PNG file on gimp for editing: 413 | .Bd -literal -offset indent 414 | $ plumb -edit /home/user/photo.png 415 | .Ed 416 | .Sh SEE ALSO 417 | .Rs 418 | .%A "Rob Pike" 419 | .%T "Plumbing and Other Utilities" 420 | .%I "Bell Laboratories" 421 | .Re 422 | .Sh HISTORY 423 | A 424 | .Nm 425 | utility appeared in the Plan 9 operating system. 426 | -------------------------------------------------------------------------------- /plumb.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | 18 | #define LEN(a) (sizeof(a) / sizeof((a)[0])) 19 | #define ENVVAR_MAX 128 20 | #define MAXTOKENS 128 21 | #define RULESPATH "/lib/plumb" 22 | #define HOME "HOME" 23 | #define OPEN_ACTION "open" 24 | #define EDIT_ACTION "edit" 25 | #define DEF_ACTION OPEN_ACTION 26 | #define ALPHANUM "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_" 27 | 28 | struct Variable { 29 | struct Variable *next; 30 | char *name; 31 | char *value; 32 | } *globals, *locals; 33 | 34 | struct Argument { 35 | struct Variable *globals, *locals; 36 | char *data; 37 | }; 38 | 39 | struct Ruleset { 40 | /* 41 | * Each block of rule, initiated by a "rules for " 42 | * line on the config file, is a ruleset. A ruleset is made of 43 | * its name (as an array of arguments) and a list of rules. 44 | * 45 | * Each rule represents a line in the config file with the form 46 | * " ". is a string that dictates the 47 | * rule's subject; is the string "matches", "types" or 48 | * "with"; and is an array of argument strings. 49 | * 50 | * A rule of type "matches" has a special argument. Its first 51 | * argument is a regular expression, while the following 52 | * optional arguments are strings. 53 | */ 54 | struct Ruleset *next; 55 | struct Rule { 56 | struct Ruleset *set; 57 | struct Rule *next; 58 | char *subj; 59 | enum Type { 60 | RULE_MATCHES, 61 | RULE_TYPES, 62 | RULE_WITH, 63 | RULE_AT, 64 | } type; 65 | regex_t reg; /* only used in "matches" rules */ 66 | char **argv; 67 | size_t argc; 68 | } *rules; 69 | char **argv; 70 | size_t argc; 71 | }; 72 | 73 | struct Parsectx { 74 | /* 75 | * Context for parsing the config file. 76 | */ 77 | FILE *fp; 78 | char *filename; 79 | size_t lineno; 80 | bool goterror; 81 | }; 82 | 83 | extern char **environ; 84 | 85 | static magic_t magic; 86 | 87 | static void 88 | usage(void) 89 | { 90 | (void)fprintf(stderr, "usage: plumb [-actions] arg...\n"); 91 | exit(EXIT_FAILURE); 92 | } 93 | 94 | static void * 95 | erealloc(void *p, size_t size) 96 | { 97 | if ((p = realloc(p, size)) == NULL) 98 | err(EXIT_FAILURE, "realloc"); 99 | return p; 100 | } 101 | 102 | static void * 103 | ecalloc(size_t nmemb, size_t size) 104 | { 105 | void *p; 106 | 107 | if ((p = calloc(nmemb, size)) == NULL) 108 | err(EXIT_FAILURE, "calloc"); 109 | return p; 110 | } 111 | 112 | static void * 113 | emalloc(size_t size) 114 | { 115 | void *p; 116 | 117 | if ((p = malloc(size)) == NULL) 118 | err(EXIT_FAILURE, "malloc"); 119 | return p; 120 | } 121 | 122 | static char * 123 | estrdup(const char *s) 124 | { 125 | char *p; 126 | 127 | if ((p = strdup(s)) == NULL) 128 | err(EXIT_FAILURE, "strdup"); 129 | return p; 130 | } 131 | 132 | static char * 133 | estrndup(const char *s, size_t maxlen) 134 | { 135 | char *p; 136 | 137 | if ((p = strndup(s, maxlen)) == NULL) 138 | err(EXIT_FAILURE, "strndup"); 139 | return p; 140 | } 141 | 142 | static pid_t 143 | efork(void) 144 | { 145 | pid_t pid; 146 | 147 | if ((pid = fork()) < 0) 148 | err(EXIT_FAILURE, "fork"); 149 | return pid; 150 | } 151 | 152 | static int 153 | beginsruleset(char **toks, size_t ntoks) 154 | { 155 | return ntoks > 2 && 156 | strcmp(toks[0], "rules") == 0 && 157 | strcmp(toks[1], "for") == 0; 158 | } 159 | 160 | static void 161 | syntaxerr(struct Parsectx *parse, const char *msg, const char *arg) 162 | { 163 | if (arg == NULL) { 164 | warnx( 165 | "%s:%zu: syntax error: %s", 166 | parse->filename, 167 | parse->lineno, 168 | msg 169 | ); 170 | } else { 171 | warnx( 172 | "%s:%zu: syntax error: %s \"%s\"", 173 | parse->filename, 174 | parse->lineno, 175 | msg, 176 | arg 177 | ); 178 | } 179 | parse->goterror = true; 180 | } 181 | 182 | static struct Rule * 183 | newrule(struct Parsectx *parse, char *toks[], size_t ntoks) 184 | { 185 | struct Rule *rule; 186 | regex_t reg; 187 | size_t i, n; 188 | int flags, res; 189 | enum Type type; 190 | char errbuf[1024]; 191 | 192 | assert(ntoks > 2); /* we have handled ntoks <= 2 before */ 193 | flags = REG_EXTENDED; 194 | if (strcmp(toks[1], "matches") == 0) { 195 | type = RULE_MATCHES; 196 | } else if (strcmp(toks[1], "imatches") == 0) { 197 | flags |= REG_ICASE; 198 | type = RULE_MATCHES; 199 | } else if (strcmp(toks[1], "types") == 0) { 200 | type = RULE_TYPES; 201 | } else if (strcmp(toks[1], "with") == 0) { 202 | type = RULE_WITH; 203 | } else if (strcmp(toks[1], "at") == 0) { 204 | type = RULE_AT; 205 | } else { 206 | syntaxerr(parse, "unknown predicate", toks[1]); 207 | return NULL; 208 | } 209 | memset(®, 0, sizeof(reg)); 210 | n = 2; 211 | switch (type) { 212 | case RULE_MATCHES: 213 | n++; 214 | if (ntoks > 3) { 215 | if (strcmp(toks[3], "into") != 0) { 216 | syntaxerr(parse, "unknown argument", toks[3]); 217 | return NULL; 218 | } 219 | n++; 220 | } 221 | if ((res = regcomp(®, toks[2], flags)) != 0) { 222 | if (regerror(res, ®, errbuf, sizeof(errbuf)) > 0) 223 | syntaxerr(parse, errbuf, NULL); 224 | else 225 | syntaxerr(parse, "wrong regex", toks[2]); 226 | regfree(®); 227 | return NULL; 228 | } 229 | break; 230 | case RULE_TYPES: 231 | case RULE_AT: 232 | if (ntoks != 3) { 233 | syntaxerr(parse, "improper number of arguments", NULL); 234 | return NULL; 235 | } 236 | break; 237 | case RULE_WITH: 238 | /* we deal with this later */ 239 | break; 240 | } 241 | rule = emalloc(sizeof(*rule)); 242 | *rule = (struct Rule){ 243 | .set = NULL, 244 | .type = type, 245 | .reg = reg, 246 | .argv = NULL, 247 | .argc = 0, 248 | .subj = toks[0], 249 | }; 250 | if (n < ntoks) { 251 | rule->argc = ntoks - n; 252 | rule->argv = ecalloc(rule->argc, sizeof(*rule->argv)); 253 | memcpy(rule->argv, toks + n, rule->argc * sizeof(*rule->argv)); 254 | } 255 | for (i = 1; i < n; i++) 256 | free(toks[i]); 257 | return rule; 258 | } 259 | 260 | static void 261 | insertvar(struct Variable **head, char *name, char *value) 262 | { 263 | struct Variable *var; 264 | 265 | if (name == NULL) { 266 | free(value); 267 | return; 268 | } 269 | for (var = *head; var != NULL; var = var->next) { 270 | if (strcmp(var->name, name) == 0) { 271 | free(var->value); 272 | var->value = value; 273 | return; 274 | } 275 | } 276 | var = emalloc(sizeof(*var)); 277 | *var = (struct Variable){ 278 | .name = name, 279 | .value = value, 280 | }; 281 | var->next = *head; 282 | *head = var; 283 | } 284 | 285 | static char * 286 | lookupvar(struct Variable *var, const char *name) 287 | { 288 | if (name == NULL) 289 | return NULL; 290 | for ( ; var != NULL; var = var->next) 291 | if (strcmp(var->name, name) == 0) 292 | return var->value != NULL ? var->value : ""; 293 | return getenv(name); 294 | } 295 | 296 | static size_t 297 | strnrspn(char *buf, char *charset, size_t len) 298 | { 299 | while (len > 0 && strchr(charset, buf[len - 1]) != NULL) 300 | len--; 301 | return len; 302 | } 303 | 304 | static size_t 305 | tokenize(struct Parsectx *parse, struct Variable *env, 306 | char *str, char *toks[], size_t maxtoks) 307 | { 308 | char *buf, *val; 309 | size_t ntoks, bufsize, len; 310 | size_t j, k; 311 | char c; 312 | bool inquote = false; 313 | 314 | ntoks = 0; 315 | bufsize = 0; 316 | buf = NULL; 317 | while (*str != '\0' && ntoks < maxtoks) { 318 | str += strspn(str, " \t"); 319 | j = 0; 320 | while (*str != '\0') { 321 | if (!inquote && strchr(" \t", *str)) 322 | break; 323 | val = NULL; 324 | if (!inquote && str[0] == '$' && str[1] == '{') { 325 | /* ${VARIABLE} */ 326 | str += 2; 327 | k = strcspn(str, "}"); 328 | if (k == 0) { 329 | syntaxerr(parse, "bad substitution", "${}"); 330 | goto error; 331 | } 332 | if (str[k] != '}') { 333 | syntaxerr(parse, "unmatching bracket", NULL); 334 | goto error; 335 | } 336 | str[k] = '\0'; 337 | if ((val = lookupvar(env, str)) == NULL) 338 | val = ""; 339 | len = strlen(val); 340 | str += k + 1; 341 | } else if (!inquote && str[0] == '$' && str[1] == '$') { 342 | /* $$ */ 343 | str += 2; 344 | val = "$"; 345 | len = 1; 346 | } else if (!inquote && str[0] == '$') { 347 | /* $VARIABLE */ 348 | str++; 349 | k = strspn(str, ALPHANUM); 350 | if (k == 0) { 351 | syntaxerr(parse, "bad substitution", "$"); 352 | goto error; 353 | } 354 | c = str[k]; 355 | str[k] = '\0'; 356 | if ((val = lookupvar(env, str)) == NULL) 357 | val = ""; 358 | len = strlen(val); 359 | str += k; 360 | if (c != '\0') { 361 | str[0] = c; 362 | } 363 | } else if (inquote && str[0] == '\'' && str[1] == '\'') { 364 | /* '' */ 365 | str += 2; 366 | val = "'"; 367 | len = 1; 368 | } else if (str[0] == '\'') { 369 | /* ' */ 370 | inquote = !inquote; 371 | str++; 372 | continue; 373 | } else { 374 | /* char */ 375 | val = str; 376 | len = 1; 377 | str++; 378 | } 379 | if (j + len + 1 > bufsize) { 380 | bufsize += len + 128; 381 | buf = erealloc(buf, bufsize); 382 | } 383 | memcpy(buf + j, val, len); 384 | j += len; 385 | } 386 | if (buf == NULL) 387 | return 0; 388 | buf[j] = '\0'; 389 | toks[ntoks++] = estrdup(buf); 390 | } 391 | free(buf); 392 | return ntoks; 393 | error: 394 | free(buf); 395 | for (j = 0; j < ntoks; j++) 396 | free(toks[j]); 397 | return 0; 398 | } 399 | 400 | static void 401 | freevars(struct Variable *head, bool freename) 402 | { 403 | struct Variable *tmp; 404 | 405 | while (head != NULL) { 406 | tmp = head; 407 | head = head->next; 408 | if (freename) 409 | free(tmp->name); 410 | free(tmp->value); 411 | free(tmp); 412 | } 413 | } 414 | 415 | static struct Ruleset * 416 | readrules(struct Parsectx *parse) 417 | { 418 | struct Rule *rule = NULL, *currule; 419 | struct Ruleset *head = NULL, *curset, *set; 420 | struct Variable *env = NULL; 421 | ssize_t linelen; 422 | size_t ntoks, n, i; 423 | size_t linesize = 0; 424 | char *toks[MAXTOKENS]; 425 | char *line = NULL; 426 | char *str = NULL; 427 | 428 | head = curset = emalloc(sizeof(*curset)); 429 | *curset = (struct Ruleset){ 0 }; 430 | while ((linelen = getline(&line, &linesize, parse->fp)) != -1) { 431 | parse->lineno++; 432 | str = line; 433 | n = strspn(line, " \t"); 434 | str += n; 435 | linelen -= n; 436 | str[strnrspn(line, " \t\n", linelen)] = '\0'; 437 | if (str[0] == '\0' || str[0] == '#') 438 | continue; 439 | if ((ntoks = tokenize(parse, env, str, toks, LEN(toks))) == 0) 440 | continue; 441 | if (ntoks < 3) { 442 | syntaxerr(parse, "improper rule", NULL); 443 | goto error; 444 | } 445 | if (toks[1][0] == '=' && toks[1][1] == '\0') { 446 | /* variable assignment */ 447 | if (ntoks != 3) { 448 | syntaxerr(parse, "bad assignment", NULL); 449 | goto error; 450 | } 451 | insertvar(&env, toks[0], toks[2]); 452 | free(toks[1]); /* free "=" */ 453 | continue; 454 | } else if (beginsruleset(toks, ntoks)) { 455 | /* a new ruleset begins */ 456 | set = emalloc(sizeof(*set)); 457 | *set = (struct Ruleset){ 458 | .next = NULL, 459 | .rules = NULL, 460 | .argv = NULL, 461 | .argc = ntoks - 2, 462 | }; 463 | set->argv = ecalloc(set->argc, sizeof(*set->argv)); 464 | memcpy(set->argv, toks+2, set->argc*sizeof(*set->argv)); 465 | free(toks[0]); /* free "rules" */ 466 | free(toks[1]); /* free "for" */ 467 | curset->next = set; 468 | curset = set; 469 | } else if ((rule = newrule(parse, toks, ntoks)) != NULL) { 470 | /* new rule for current ruleset */ 471 | rule->set = curset; 472 | if (curset->rules == NULL) 473 | curset->rules = rule; 474 | else 475 | currule->next = rule; 476 | currule = rule; 477 | } else { 478 | syntaxerr(parse, "improper rule", NULL); 479 | error: 480 | for (i = 0; i < ntoks; i++) { 481 | free(toks[i]); 482 | } 483 | } 484 | } 485 | freevars(env, true); 486 | free(line); 487 | return head; 488 | } 489 | 490 | static char * 491 | getconfig(void) 492 | { 493 | char *home, *filename; 494 | size_t size; 495 | 496 | if ((home = getenv(HOME)) == NULL) 497 | errx(EXIT_FAILURE, "could not find $HOME"); 498 | size = strlen(home) + sizeof(RULESPATH); 499 | filename = emalloc(size); 500 | (void)snprintf(filename, size, "%s" RULESPATH, home); 501 | return filename; 502 | 503 | } 504 | 505 | static void 506 | freerules(struct Ruleset *head) 507 | { 508 | struct Ruleset *set; 509 | struct Rule *rule; 510 | size_t i; 511 | 512 | while (head != NULL) { 513 | set = head; 514 | head = set->next; 515 | while (set->rules != NULL) { 516 | rule = set->rules; 517 | set->rules = rule->next; 518 | if (rule->type == RULE_MATCHES) 519 | regfree(&rule->reg); 520 | for (i = 0; i < rule->argc; i++) 521 | free(rule->argv[i]); 522 | free(rule->argv); 523 | free(rule->subj); 524 | free(rule); 525 | } 526 | for (i = 0; i < set->argc; i++) 527 | free(set->argv[i]); 528 | free(set->argv); 529 | free(set); 530 | } 531 | } 532 | 533 | static char * 534 | lookupargvar(struct Variable *globals, struct Variable *locals, 535 | char *data, char *name) 536 | { 537 | char *value; 538 | 539 | value = lookupvar(locals, name); 540 | if (value == NULL) 541 | value = lookupvar(globals, name); 542 | if (value == NULL && strcmp("data", name) == 0) 543 | value = data; 544 | if (value == NULL) 545 | value = ""; 546 | return value; 547 | } 548 | 549 | static struct Rule * 550 | matchruleset(struct Ruleset *set, char *arg, 551 | char **actions, int nactions, 552 | struct Variable *globals, struct Variable **locals_ret) 553 | { 554 | struct Variable *locals = NULL; 555 | struct Rule *rule = NULL; 556 | struct stat sb; 557 | regmatch_t pmatch[MAXTOKENS]; 558 | regoff_t beg, len; 559 | size_t i; 560 | int j; 561 | char *value, *newstr; 562 | const char *filetype; 563 | bool match = true; 564 | 565 | for (rule = set->rules; rule != NULL; rule = rule->next) { 566 | if (rule->type == RULE_WITH) 567 | continue; /* we handle RULE_WITH later */ 568 | value = lookupargvar(globals, locals, arg, rule->subj); 569 | if (rule->type == RULE_TYPES) { 570 | if (stat(value, &sb) == -1) 571 | filetype = NULL; 572 | else if (S_ISDIR(sb.st_mode)) 573 | filetype = "inode/directory"; 574 | else 575 | filetype = magic_file(magic, value); 576 | if (filetype != NULL) { 577 | newstr = estrdup(filetype); 578 | insertvar(&locals, rule->argv[0], newstr); 579 | } else { 580 | insertvar(&locals, rule->argv[0], NULL); 581 | } 582 | continue; 583 | } 584 | if (rule->type == RULE_AT) { 585 | insertvar( 586 | &locals, 587 | rule->argv[0], 588 | realpath(value, NULL) /* freed later */ 589 | ); 590 | continue; 591 | } 592 | /* rule->type == RULE_MATCHES */ 593 | if (regexec(&rule->reg, value, MAXTOKENS, pmatch, 0) != 0) { 594 | match = false; 595 | continue; 596 | } 597 | if (pmatch[0].rm_so != 0 || value[pmatch[0].rm_eo] != '\0') { 598 | match = false; 599 | continue; 600 | } 601 | for (i = 0; i < rule->reg.re_nsub && i < rule->argc; i++) { 602 | beg = pmatch[i + 1].rm_so; 603 | len = pmatch[i + 1].rm_eo - beg; 604 | if (beg >= 0 && len >= 0) { 605 | newstr = estrndup(value + beg, len); 606 | insertvar(&locals, rule->argv[i], newstr); 607 | } else { 608 | insertvar(&locals, rule->argv[i], NULL); 609 | } 610 | } 611 | } 612 | if (locals_ret != NULL) 613 | *locals_ret = locals; 614 | else 615 | freevars(locals, false); 616 | if (!match) 617 | return NULL; 618 | for (rule = set->rules; rule != NULL; rule = rule->next) { 619 | if (rule->type != RULE_WITH) 620 | continue; 621 | for (j = 0; j < nactions; j++) { 622 | if (strcmp(actions[j], rule->subj) == 0) { 623 | return rule; 624 | } 625 | } 626 | } 627 | return NULL; 628 | } 629 | 630 | static void 631 | plumb(struct Rule *rule, struct Argument *args, int argc) 632 | { 633 | char **newargv = NULL; 634 | char **cmd, **p; 635 | char *buf = NULL; 636 | char *str, *var, *val; 637 | size_t len, k; 638 | size_t pos = 0; 639 | size_t size; 640 | size_t bufsize = 0; 641 | int newargc = 0; 642 | int i; 643 | char ch; 644 | bool gotvar = false; 645 | 646 | if (rule == NULL) { 647 | warnx("could not find rule for arguments"); 648 | return; 649 | } 650 | 651 | /* 652 | * Anounce the ruleset we are plumbing. 653 | */ 654 | (void)fprintf(stderr, "plumbing "); 655 | for (k = 0; k < rule->set->argc; k++) { 656 | (void)fprintf( 657 | stderr, 658 | "%s%s", 659 | (k == 0 ? "" : " "), 660 | rule->set->argv[k] 661 | ); 662 | } 663 | (void)fprintf(stderr, "\n"); 664 | 665 | /* 666 | * Find the substring "%var" in argv[i], where i is the last 667 | * element of argv[]. 668 | */ 669 | str = rule->argv[rule->argc - 1]; 670 | len = 0; 671 | var = NULL; 672 | while (*str != '\0') { 673 | if (!gotvar && str[0] == '%' && str[1] == '{') { 674 | /* %{VARIABLE} */ 675 | k = strcspn(str + 2, "}"); 676 | if (k == 0 || str[k + 2] != '}') 677 | goto fallback; 678 | str += 2; 679 | str[k] = '\0'; 680 | var = str; 681 | str += k + 1; 682 | gotvar = true; 683 | pos = len; 684 | continue; 685 | } else if (!gotvar && str[0] == '%') { 686 | /* %VARIABLE */ 687 | k = strspn(str + 1, ALPHANUM); 688 | if (k == 0) 689 | goto fallback; 690 | str++; 691 | ch = str[k]; 692 | str[k] = '\0'; 693 | var = str; 694 | str += k; 695 | if (ch != '\0') 696 | str[0] = ch; 697 | pos = len; 698 | gotvar = true; 699 | continue; 700 | } else if (!gotvar && str[0] == '%' && str[1] == '%') { 701 | /* %% */ 702 | ch = '%'; 703 | str += 2; 704 | } else { 705 | fallback: 706 | /* char */ 707 | ch = *str; 708 | str++; 709 | } 710 | if (len + 2 > bufsize) { 711 | bufsize += len + 128; 712 | buf = erealloc(buf, bufsize); 713 | } 714 | buf[len++] = ch; 715 | } 716 | if (buf != NULL) { 717 | buf[len] = '\0'; 718 | str = buf; 719 | } else { 720 | str = ""; 721 | pos = len = 0; 722 | } 723 | 724 | /* 725 | * Create the command cmd[] to be spawned. 726 | * 727 | * If rule->argv[rule->argc - 1] (the last argument of the 728 | * plumbing rule) does not contain the "%var" substring, cmd[] 729 | * is exactly the plumbing rule itself (cmd == rule->argv). 730 | * 731 | * However, if rule->argv[rule->argc - 1] (the last argument of 732 | * the plumbing rule) contains the "%var" substring, cmd[] is 733 | * composed of: 734 | * - The strings rule->argv[0] to rule->argv[rule->argc - 2]. 735 | * - n strings (n is the number of command-line arguments given 736 | * to plumb), each one equal to rule->argv[rule->argc - 1], 737 | * but with the "%var" substring replaced with the value that 738 | * "%var" has for each command-line argument. 739 | * 740 | * For example, if plumb is invoked as 741 | * 742 | * $ plumb -edit foo.png file:///var/www/bar.jpg 743 | * 744 | * And the rule for editing image files is 745 | * 746 | * gimp -s -- %path 747 | * 748 | * And the value of the variable %path for the first argument is 749 | * 750 | * foo.png 751 | * 752 | * And the value of %path for the second argument is 753 | * 754 | * /var/www/bar.jpg 755 | * 756 | * Then, the command to be executed is 757 | * 758 | * gimp -s -- foo.png /var/www/bar.jpg 759 | */ 760 | p = NULL; 761 | cmd = rule->argv; 762 | if (var != NULL) { 763 | newargc = argc, 764 | newargv = ecalloc(newargc, sizeof(*newargv)); 765 | for (i = 0; i < argc; i++) { 766 | val = lookupargvar( 767 | args[i].globals, 768 | args[i].locals, 769 | args[i].data, 770 | var 771 | ); 772 | size = len + strlen(val) + 1; 773 | newargv[i] = emalloc(size); 774 | (void)snprintf( 775 | newargv[i], 776 | size, 777 | "%.*s%s%.*s", 778 | (int)pos, 779 | str, 780 | val, 781 | (int)(len - pos), 782 | str + pos 783 | ); 784 | } 785 | p = ecalloc(rule->argc + newargc, sizeof(*p)); 786 | cmd = p; 787 | for (k = 0; k < rule->argc - 1; k++) 788 | cmd[k] = rule->argv[k]; 789 | for (k = 0; k < (size_t)newargc; k++) 790 | cmd[rule->argc - 1 + k] = newargv[k]; 791 | cmd[rule->argc + newargc - 1] = NULL; 792 | } 793 | 794 | if (efork() == 0) { 795 | if (posix_spawnp(NULL, cmd[0], NULL, NULL, cmd, environ)) 796 | err(EXIT_FAILURE, "posix_spawnp"); 797 | exit(EXIT_SUCCESS); 798 | } 799 | free(p); 800 | free(buf); 801 | for (i = 0; i < newargc; i++) 802 | free(newargv[i]); 803 | free(newargv); 804 | } 805 | 806 | static void 807 | freeargs(struct Argument *args, int argc) 808 | { 809 | int i; 810 | 811 | for (i = 0; i < argc; i++) { 812 | freevars(args[i].globals, false); 813 | freevars(args[i].locals, false); 814 | } 815 | free(args); 816 | } 817 | 818 | int 819 | main(int argc, char *argv[]) 820 | { 821 | struct Ruleset *sets, *set; 822 | struct Rule *plumbwith = NULL; 823 | struct Rule *newaction = NULL; 824 | struct Argument *args; 825 | struct Parsectx parse = { 0 }; 826 | size_t span; 827 | char **actions; 828 | int nactions; 829 | int i; 830 | 831 | actions = &argv[1]; 832 | nactions = 0; 833 | for (i = 1; i < argc; i++) { 834 | span = strspn(argv[i], "-"); 835 | if (span == 0) /* argv[i] is not -word */ 836 | break; 837 | if (argv[i][span] == '\0') { /* argv[i] is -- */ 838 | i++; 839 | break; 840 | } 841 | if (strcmp(argv[i], "-o") == 0) /* argv[i] is -o */ 842 | argv[i] = OPEN_ACTION; 843 | else if (strcmp(argv[i], "-e") == 0) /* argv[i] is -e */ 844 | argv[i] = EDIT_ACTION; 845 | else /* argv[i] is -word */ 846 | argv[i]++; 847 | nactions++; 848 | } 849 | argc -= i; 850 | argv += i; 851 | if (argc == 0) 852 | usage(); 853 | if (nactions == 0) { 854 | actions = (char *[]){ DEF_ACTION }; 855 | nactions = 1; 856 | } 857 | args = ecalloc(argc, sizeof(*args)); 858 | magic = magic_open( 859 | MAGIC_SYMLINK | MAGIC_MIME_TYPE | 860 | MAGIC_PRESERVE_ATIME | MAGIC_ERROR 861 | ); 862 | if (magic == NULL) 863 | errx(EXIT_FAILURE, "could not get magic cookie"); 864 | if (magic_load(magic, NULL) == -1) 865 | errx(EXIT_FAILURE, "could not load magic database"); 866 | parse.filename = getconfig(); 867 | if ((parse.fp = fopen(parse.filename, "r")) == NULL) 868 | err(EXIT_FAILURE, "%s", parse.filename); 869 | sets = readrules(&parse); 870 | for (i = 0; i < argc; i++) { 871 | /* 872 | * First we run on the top ruleset, which should contain 873 | * no action rule, so we can fill in the global argument 874 | * variables. 875 | */ 876 | args[i].data = argv[i]; 877 | args[i].globals = NULL; 878 | args[i].locals = NULL; 879 | set = sets; 880 | (void)matchruleset( 881 | set, argv[i], 882 | actions, nactions, 883 | NULL, &args[i].globals 884 | ); 885 | if (i == 0) { 886 | /* 887 | * For the first argument, we find a ruleset 888 | * matching it. 889 | */ 890 | while ((set = set->next) != NULL) { 891 | plumbwith = matchruleset( 892 | set, argv[i], 893 | actions, nactions, 894 | args[i].globals, &args[i].locals 895 | ); 896 | if (plumbwith != NULL) 897 | break; 898 | freevars(args[i].locals, false); 899 | args[i].locals = NULL; 900 | } 901 | if (plumbwith == NULL) { 902 | break; 903 | } 904 | } else { 905 | /* 906 | * For the following arguments, we check whether 907 | * the it matchs the ruleset of the 1st argument. 908 | */ 909 | newaction = matchruleset( 910 | plumbwith->set, argv[i], 911 | actions, nactions, 912 | args[i].globals, &args[i].locals 913 | ); 914 | if (plumbwith != newaction) { 915 | freevars(args[i].locals, false); 916 | args[i].locals = NULL; 917 | plumbwith = NULL; 918 | break; 919 | } 920 | } 921 | } 922 | magic_close(magic); 923 | plumb(plumbwith, args, argc); 924 | freerules(sets); 925 | free(parse.filename); 926 | freeargs(args, argc); 927 | return 0; 928 | } 929 | --------------------------------------------------------------------------------