diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:25:26 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:25:26 +0300 |
| commit | 5c7294b9667f0a38005272add8c78483b8658092 (patch) | |
| tree | 5916ac24ae1c67c72b96a6e6fd745dc25ff1187d /corebinutils/expr | |
| parent | f0f174696b38adfd6063bb74e5b48d2b864cb650 (diff) | |
| parent | 23bf7a79f51567e56bfb4fc657e3dc66c36b41de (diff) | |
| download | Project-Tick-5c7294b9667f0a38005272add8c78483b8658092.tar.gz Project-Tick-5c7294b9667f0a38005272add8c78483b8658092.zip | |
Add 'corebinutils/expr/' from commit '23bf7a79f51567e56bfb4fc657e3dc66c36b41de'
git-subtree-dir: corebinutils/expr
git-subtree-mainline: f0f174696b38adfd6063bb74e5b48d2b864cb650
git-subtree-split: 23bf7a79f51567e56bfb4fc657e3dc66c36b41de
Diffstat (limited to 'corebinutils/expr')
| -rw-r--r-- | corebinutils/expr/.gitignore | 25 | ||||
| -rw-r--r-- | corebinutils/expr/GNUmakefile | 35 | ||||
| -rw-r--r-- | corebinutils/expr/LICENSE | 202 | ||||
| -rw-r--r-- | corebinutils/expr/LICENSES/Apache-2.0.txt | 73 | ||||
| -rw-r--r-- | corebinutils/expr/README.md | 35 | ||||
| -rw-r--r-- | corebinutils/expr/expr.1 | 173 | ||||
| -rw-r--r-- | corebinutils/expr/expr.c | 923 | ||||
| -rw-r--r-- | corebinutils/expr/tests/test.sh | 206 |
8 files changed, 1672 insertions, 0 deletions
diff --git a/corebinutils/expr/.gitignore b/corebinutils/expr/.gitignore new file mode 100644 index 0000000000..a74d30b48c --- /dev/null +++ b/corebinutils/expr/.gitignore @@ -0,0 +1,25 @@ +*.a +*.core +*.lo +*.nossppico +*.o +*.orig +*.pico +*.pieo +*.po +*.rej +*.so +*.so.[0-9]* +*.sw[nop] +*~ +.*DS_Store +.cache +.clangd +.ccls-cache +.depend* +compile_commands.json +compile_commands.events.json +tags +build/ +out/ +.linux-obj/ diff --git a/corebinutils/expr/GNUmakefile b/corebinutils/expr/GNUmakefile new file mode 100644 index 0000000000..c1619486e5 --- /dev/null +++ b/corebinutils/expr/GNUmakefile @@ -0,0 +1,35 @@ +.DEFAULT_GOAL := all + +CC ?= cc +CPPFLAGS += -D_POSIX_C_SOURCE=200809L +CFLAGS ?= -O2 +CFLAGS += -std=c17 -g -Wall -Wextra -Werror -Wpedantic +LDFLAGS ?= +LDLIBS ?= + +OBJDIR := $(CURDIR)/build +OUTDIR := $(CURDIR)/out +TARGET := $(OUTDIR)/expr +OBJS := $(OBJDIR)/expr.o + +.PHONY: all clean dirs status test + +all: $(TARGET) + +dirs: + @mkdir -p "$(OBJDIR)" "$(OUTDIR)" + +$(TARGET): $(OBJS) | dirs + $(CC) $(LDFLAGS) -o "$@" $(OBJS) $(LDLIBS) + +$(OBJDIR)/expr.o: $(CURDIR)/expr.c | dirs + $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/expr.c" -o "$@" + +test: $(TARGET) + EXPR_BIN="$(TARGET)" sh "$(CURDIR)/tests/test.sh" + +status: + @printf '%s\n' "$(TARGET)" + +clean: + @rm -rf "$(OBJDIR)" "$(OUTDIR)" diff --git a/corebinutils/expr/LICENSE b/corebinutils/expr/LICENSE new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/corebinutils/expr/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/corebinutils/expr/LICENSES/Apache-2.0.txt b/corebinutils/expr/LICENSES/Apache-2.0.txt new file mode 100644 index 0000000000..137069b823 --- /dev/null +++ b/corebinutils/expr/LICENSES/Apache-2.0.txt @@ -0,0 +1,73 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/corebinutils/expr/README.md b/corebinutils/expr/README.md new file mode 100644 index 0000000000..5ac54a8055 --- /dev/null +++ b/corebinutils/expr/README.md @@ -0,0 +1,35 @@ +# expr + +Standalone musl-libc-based Linux port of FreeBSD `expr` for Project Tick BSD/Linux Distribution. + +## Build + +```sh +gmake -f GNUmakefile +gmake -f GNUmakefile CC=musl-gcc +``` + +## Test + +```sh +gmake -f GNUmakefile test +gmake -f GNUmakefile test CC=musl-gcc +``` + +## Port Strategy + +- The standalone layout matches sibling Linux ports such as `bin/cat`, `bin/echo`, and `bin/chmod`: local `GNUmakefile`, local shell tests, and no dependency on the top-level FreeBSD build. +- The original FreeBSD yacc grammar was translated into a local recursive-descent parser. This removes the FreeBSD yacc build dependency and keeps precedence, associativity, exit codes, and value semantics explicit in one musl-clean source file. +- Overflow handling no longer depends on signed-wrap compiler behavior. Integer `+`, `-`, `*`, `/`, and `%` use explicit checked arithmetic around `intmax_t`. +- The `:` operator stays on POSIX `regcomp(3)` / `regexec(3)` / `regfree(3)`, and string comparisons stay on locale-aware `strcoll(3)` after `setlocale(LC_ALL, "")`. + +## Linux Semantics + +- Supported: POSIX option parsing with `--` for a leading negative first operand. +- Supported: explicit compatibility mode via `-e`, matching FreeBSD's relaxed numeric parsing rules for optional leading `+`, leading whitespace, and empty-string-as-zero in numeric contexts. +- Supported: BRE matching for `:` using the active libc regex engine. + +## Intentionally Unsupported Semantics + +- FreeBSD `check_utility_compat("expr")` auto-compat probing is not implemented. Linux builds do not carry that FreeBSD userland compatibility hook. +- The legacy `EXPR_COMPAT` environment toggle is not implemented. On Linux the compatibility mode is explicit and reproducible through `-e`. diff --git a/corebinutils/expr/expr.1 b/corebinutils/expr/expr.1 new file mode 100644 index 0000000000..bd186059a7 --- /dev/null +++ b/corebinutils/expr/expr.1 @@ -0,0 +1,173 @@ +.\" SPDX-License-Identifier: Apache-2.0 +.\" +.\" Copyright (c) 2026 Project Tick +.\" +.\" Licensed under the Apache License, Version 2.0 (the "License"); +.\" you may not use this file except in compliance with the License. +.\" You may obtain a copy of the License at +.\" +.\" http://www.apache.org/licenses/LICENSE-2.0 +.\" +.\" Unless required by applicable law or agreed to in writing, software +.\" distributed under the License is distributed on an "AS IS" BASIS, +.\" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +.\" See the License for the specific language governing permissions and +.\" limitations under the License. +.\" +.\" Copyright (c) 2026 +.\" Project Tick +.Dd February 28, 2026 +.Dt EXPR 1 +.Os Linux +.Sh NAME +.Nm expr +.Nd evaluate integer, string, and regular-expression expressions +.Sh SYNOPSIS +.Nm +.Op Fl e +.Op Fl - +.Ar expression ... +.Sh DESCRIPTION +.Nm +evaluates the operand vector given on the command line and writes the result +to standard output. +Each operator and operand must be passed as a separate shell argument. +.Pp +This Linux port is a standalone implementation derived from FreeBSD +semantics, but it does not depend on the FreeBSD build system or +compatibility probes. +.Sh OPTIONS +.Bl -tag -width "-e" +.It Fl e +Enable compatibility parsing for numeric operands. +In this mode, +.Nm +accepts leading white space, an optional leading plus sign, and an empty +string in numeric context. +.It Fl - +End option processing. +Use +.Fl - +when the first operand may begin with +.Ql - . +.El +.Sh OPERANDS +Operators are evaluated from low precedence to high precedence in the +following order. +Operators with the same precedence associate left to right. +.Bl -tag -width "expr1 <= expr2" +.It Ar expr1 Li \&| Ar expr2 +Return +.Ar expr1 +if it is neither the empty string nor numeric zero. +Otherwise return +.Ar expr2 +if it is non-empty and non-zero. +If both sides are null or zero, return +.Li 0 . +.It Ar expr1 Li & Ar expr2 +Return +.Ar expr1 +if both operands are non-empty and non-zero. +Otherwise return +.Li 0 . +.It Ar expr1 Li = Ar expr2 +.It Ar expr1 Li != Ar expr2 +.It Ar expr1 Li \&< Ar expr2 +.It Ar expr1 Li \&<= Ar expr2 +.It Ar expr1 Li \&> Ar expr2 +.It Ar expr1 Li \&>= Ar expr2 +If both operands are valid integers, compare them numerically. +Otherwise compare them as strings using the current locale collation rules. +The result is +.Li 1 +for true and +.Li 0 +for false. +.It Ar expr1 Li + Ar expr2 +.It Ar expr1 Li - Ar expr2 +.It Ar expr1 Li * Ar expr2 +.It Ar expr1 Li / Ar expr2 +.It Ar expr1 Li % Ar expr2 +Perform checked arithmetic on +.Vt intmax_t . +Division and remainder by zero are rejected. +Overflow terminates the utility with an error. +.It Ar expr1 Li : Ar expr2 +Match +.Ar expr1 +against the basic regular expression +.Ar expr2 . +The match is anchored at the start of the string. +If the pattern contains a capturing subexpression and the match succeeds, +the first captured substring is returned. +If the pattern has no capturing subexpression and the match succeeds, +the length of the match is returned. +On a failed match, the result is the empty string when the pattern has a +capturing subexpression, otherwise +.Li 0 . +.El +.Pp +Parentheses may be used for grouping. +Because +.Nm +does not lexically separate operators from operands, a token that is +identical to an operator must be disambiguated by shell quoting or by +rewriting the expression. +.Sh SEMANTICS +By default, integer operands must be base-10 strings consisting of an +optional leading minus sign followed by digits. +In compatibility mode, +.Fl e , +leading white space and a leading plus sign are accepted, and the empty +string is treated as zero in numeric context. +.Pp +This Linux port supports +.Fl - +for POSIX-style end-of-options handling. +It does not implement FreeBSD +.Xr check_utility_compat 3 +probing or the historical +.Ev EXPR_COMPAT +environment switch. +.Sh EXIT STATUS +.Bl -tag -width indent -compact +.It 0 +The final value is neither empty nor zero. +.It 1 +The final value is the empty string or numeric zero. +.It 2 +The expression is invalid or evaluation failed. +.El +.Sh DIAGNOSTICS +Errors are reported on standard error. +The implementation emits explicit diagnostics for syntax errors, invalid +decimal operands, arithmetic overflow, division by zero, regular-expression +compilation failures, and write failures. +.Sh EXAMPLES +.Bl -bullet +.It +Increment a shell variable: +.Dl n=$(expr "$n" + 1) +.It +Safely handle a leading negative operand: +.Dl n=$(expr -- "$n" + 1) +.It +Return the basename component of a path: +.Dl expr "//$path" : '.*/\e(.*\e)' +.It +Return the length of a shell variable: +.Dl expr \&( "X$text" : '.*' \&) - 1 +.El +.Sh SEE ALSO +.Xr sh 1 , +.Xr test 1 , +.Xr regex 3 +.Sh STANDARDS +The strict parsing mode follows the POSIX utility syntax model for option +handling and preserves the historical +.Nm +operator set. +Compatibility mode +.Pq Fl e +is a local extension for FreeBSD-script migration. diff --git a/corebinutils/expr/expr.c b/corebinutils/expr/expr.c new file mode 100644 index 0000000000..6783f6afaf --- /dev/null +++ b/corebinutils/expr/expr.c @@ -0,0 +1,923 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright (c) 2026 Project Tick + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Linux/musl-native port of FreeBSD expr(1). + * The original parser was expressed in yacc grammar form; this port keeps + * the same operator precedence and value semantics but uses a local + * recursive-descent parser so the standalone build does not depend on the + * FreeBSD build system or generated sources. + */ + +#define _POSIX_C_SOURCE 200809L + +#include <ctype.h> +#include <errno.h> +#include <inttypes.h> +#include <limits.h> +#include <locale.h> +#include <regex.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#define ERR_EXIT 2 + +enum value_type { + VALUE_INTEGER, + VALUE_NUMERIC_STRING, + VALUE_STRING, +}; + +struct value { + enum value_type type; + union { + intmax_t integer; + char *string; + } data; +}; + +struct parser { + const char *program; + char **argv; + int argc; + int index; + bool compat_mode; +}; + +static void usage(void) __attribute__((noreturn)); +static void vfail(struct parser *parser, bool with_errno, const char *fmt, + va_list ap) __attribute__((noreturn)); +static void fail(struct parser *parser, const char *fmt, ...) + __attribute__((format(printf, 2, 3), noreturn)); +static void fail_errno(struct parser *parser, const char *fmt, ...) + __attribute__((format(printf, 2, 3), noreturn)); +static void *xmalloc(struct parser *parser, size_t size); +static char *xstrdup(struct parser *parser, const char *text); +static bool is_operator_token(const char *token); +static bool is_integer_literal(const char *text, bool compat_mode); +static struct value value_from_integer(intmax_t integer); +static struct value value_from_argument(struct parser *parser, const char *text); +static void value_dispose(struct value *value); +static struct value value_move(struct value *value); +static bool value_try_to_integer(struct value *value); +static void value_require_integer(struct parser *parser, struct value *value); +static void value_require_string(struct parser *parser, struct value *value); +static bool value_is_zero_or_null(struct value *value); +static int compare_values(struct parser *parser, struct value *left, + struct value *right); +static bool add_overflow(intmax_t left, intmax_t right, intmax_t *result); +static bool sub_overflow(intmax_t left, intmax_t right, intmax_t *result); +static uintmax_t intmax_magnitude(intmax_t value); +static bool mul_overflow(intmax_t left, intmax_t right, intmax_t *result); +static struct value value_or(struct value *left, struct value *right); +static struct value value_and(struct value *left, struct value *right); +static struct value value_compare_eq(struct parser *parser, struct value *left, + struct value *right); +static struct value value_compare_ne(struct parser *parser, struct value *left, + struct value *right); +static struct value value_compare_lt(struct parser *parser, struct value *left, + struct value *right); +static struct value value_compare_le(struct parser *parser, struct value *left, + struct value *right); +static struct value value_compare_gt(struct parser *parser, struct value *left, + struct value *right); +static struct value value_compare_ge(struct parser *parser, struct value *left, + struct value *right); +static struct value value_add(struct parser *parser, struct value *left, + struct value *right); +static struct value value_sub(struct parser *parser, struct value *left, + struct value *right); +static struct value value_mul(struct parser *parser, struct value *left, + struct value *right); +static struct value value_div(struct parser *parser, struct value *left, + struct value *right); +static struct value value_rem(struct parser *parser, struct value *left, + struct value *right); +static char *dup_substring(struct parser *parser, const char *text, regoff_t start, + regoff_t end); +static struct value value_match(struct parser *parser, struct value *left, + struct value *right); +static const char *peek_token(const struct parser *parser); +static bool match_token(struct parser *parser, const char *token); +static struct value parse_expression(struct parser *parser); +static struct value parse_or(struct parser *parser); +static struct value parse_and(struct parser *parser); +static struct value parse_compare(struct parser *parser); +static struct value parse_add(struct parser *parser); +static struct value parse_mul(struct parser *parser); +static struct value parse_match_expr(struct parser *parser); +static struct value parse_primary(struct parser *parser); +static void write_result(struct parser *parser, const struct value *value); + +static void +usage(void) +{ + fprintf(stderr, "usage: expr [-e] [--] expression\n"); + exit(ERR_EXIT); +} + +static void +vfail(struct parser *parser, bool with_errno, const char *fmt, va_list ap) +{ + int saved_errno; + + saved_errno = errno; + fprintf(stderr, "%s: ", parser->program); + vfprintf(stderr, fmt, ap); + if (with_errno) { + fprintf(stderr, ": %s", + saved_errno == 0 ? "I/O error" : strerror(saved_errno)); + } + fputc('\n', stderr); + exit(ERR_EXIT); +} + +static void +fail(struct parser *parser, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vfail(parser, false, fmt, ap); + va_end(ap); +} + +static void +fail_errno(struct parser *parser, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vfail(parser, true, fmt, ap); + va_end(ap); +} + +static void * +xmalloc(struct parser *parser, size_t size) +{ + void *memory; + + if (size == 0) { + size = 1; + } + memory = malloc(size); + if (memory == NULL) { + fail(parser, "malloc failed"); + } + return (memory); +} + +static char * +xstrdup(struct parser *parser, const char *text) +{ + size_t length; + char *copy; + + length = strlen(text) + 1; + copy = xmalloc(parser, length); + memcpy(copy, text, length); + return (copy); +} + +static bool +is_operator_token(const char *token) +{ + static const char *const operators[] = { + "|", + "&", + "=", + ">", + "<", + ">=", + "<=", + "!=", + "+", + "-", + "*", + "/", + "%", + ":", + "(", + ")", + }; + size_t i; + + for (i = 0; i < sizeof(operators) / sizeof(operators[0]); i++) { + if (strcmp(token, operators[i]) == 0) { + return (true); + } + } + return (false); +} + +static bool +is_integer_literal(const char *text, bool compat_mode) +{ + const unsigned char *cursor; + + cursor = (const unsigned char *)text; + if (compat_mode) { + if (*cursor == '\0') { + return (true); + } + while (isspace(*cursor) != 0) { + cursor++; + } + } + if (*cursor == '-' || (compat_mode && *cursor == '+')) { + cursor++; + } + if (*cursor == '\0') { + return (false); + } + while (isdigit(*cursor) != 0) { + cursor++; + } + return (*cursor == '\0'); +} + +static struct value +value_from_integer(intmax_t integer) +{ + struct value value; + + value.type = VALUE_INTEGER; + value.data.integer = integer; + return (value); +} + +static struct value +value_from_argument(struct parser *parser, const char *text) +{ + struct value value; + + value.data.string = xstrdup(parser, text); + value.type = is_integer_literal(text, parser->compat_mode) ? + VALUE_NUMERIC_STRING : VALUE_STRING; + return (value); +} + +static void +value_dispose(struct value *value) +{ + if (value->type == VALUE_NUMERIC_STRING || value->type == VALUE_STRING) { + free(value->data.string); + } + value->type = VALUE_INTEGER; + value->data.integer = 0; +} + +static struct value +value_move(struct value *value) +{ + struct value moved; + + moved = *value; + value->type = VALUE_INTEGER; + value->data.integer = 0; + return (moved); +} + +static bool +value_try_to_integer(struct value *value) +{ + intmax_t parsed; + char *end; + + if (value->type == VALUE_INTEGER) { + return (true); + } + if (value->type != VALUE_NUMERIC_STRING) { + return (false); + } + + errno = 0; + end = NULL; + parsed = strtoimax(value->data.string, &end, 10); + if (errno == ERANGE || end == NULL || *end != '\0') { + return (false); + } + + free(value->data.string); + value->type = VALUE_INTEGER; + value->data.integer = parsed; + return (true); +} + +static void +value_require_integer(struct parser *parser, struct value *value) +{ + if (value->type == VALUE_STRING) { + fail(parser, "not a decimal number: '%s'", value->data.string); + } + if (!value_try_to_integer(value)) { + fail(parser, "operand too large: '%s'", value->data.string); + } +} + +static void +value_require_string(struct parser *parser, struct value *value) +{ + int length; + size_t size; + char *buffer; + + if (value->type != VALUE_INTEGER) { + return; + } + + length = snprintf(NULL, 0, "%jd", value->data.integer); + if (length < 0) { + fail(parser, "failed to format integer result"); + } + size = (size_t)length + 1; + buffer = xmalloc(parser, size); + if (snprintf(buffer, size, "%jd", value->data.integer) != length) { + free(buffer); + fail(parser, "failed to format integer result"); + } + value->type = VALUE_STRING; + value->data.string = buffer; +} + +static bool +value_is_zero_or_null(struct value *value) +{ + if (value->type == VALUE_INTEGER) { + return (value->data.integer == 0); + } + if (value->data.string[0] == '\0') { + return (true); + } + return (value_try_to_integer(value) && value->data.integer == 0); +} + +static int +compare_values(struct parser *parser, struct value *left, struct value *right) +{ + int result; + + if (left->type == VALUE_STRING || right->type == VALUE_STRING) { + value_require_string(parser, left); + value_require_string(parser, right); + result = strcoll(left->data.string, right->data.string); + } else { + value_require_integer(parser, left); + value_require_integer(parser, right); + if (left->data.integer > right->data.integer) { + result = 1; + } else if (left->data.integer < right->data.integer) { + result = -1; + } else { + result = 0; + } + } + + value_dispose(left); + value_dispose(right); + return (result); +} + +static bool +add_overflow(intmax_t left, intmax_t right, intmax_t *result) +{ + if (right > 0 && left > INTMAX_MAX - right) { + return (true); + } + if (right < 0 && left < INTMAX_MIN - right) { + return (true); + } + *result = left + right; + return (false); +} + +static bool +sub_overflow(intmax_t left, intmax_t right, intmax_t *result) +{ + if (right > 0 && left < INTMAX_MIN + right) { + return (true); + } + if (right < 0 && left > INTMAX_MAX + right) { + return (true); + } + *result = left - right; + return (false); +} + +static uintmax_t +intmax_magnitude(intmax_t value) +{ + if (value >= 0) { + return ((uintmax_t)value); + } + return ((uintmax_t)(-(value + 1)) + 1U); +} + +static bool +mul_overflow(intmax_t left, intmax_t right, intmax_t *result) +{ + uintmax_t left_abs; + uintmax_t right_abs; + uintmax_t limit; + uintmax_t product; + bool negative; + + if (left == 0 || right == 0) { + *result = 0; + return (false); + } + + left_abs = intmax_magnitude(left); + right_abs = intmax_magnitude(right); + negative = ((left < 0) != (right < 0)); + limit = negative ? ((uintmax_t)INTMAX_MAX + 1U) : (uintmax_t)INTMAX_MAX; + + if (right_abs > limit / left_abs) { + return (true); + } + + product = left_abs * right_abs; + if (!negative) { + *result = (intmax_t)product; + return (false); + } + if (product == (uintmax_t)INTMAX_MAX + 1U) { + *result = INTMAX_MIN; + return (false); + } + *result = -(intmax_t)product; + return (false); +} + +static struct value +value_or(struct value *left, struct value *right) +{ + if (!value_is_zero_or_null(left)) { + value_dispose(right); + return (value_move(left)); + } + value_dispose(left); + if (!value_is_zero_or_null(right)) { + return (value_move(right)); + } + value_dispose(right); + return (value_from_integer(0)); +} + +static struct value +value_and(struct value *left, struct value *right) +{ + if (value_is_zero_or_null(left) || value_is_zero_or_null(right)) { + value_dispose(left); + value_dispose(right); + return (value_from_integer(0)); + } + value_dispose(right); + return (value_move(left)); +} + +static struct value +value_compare_eq(struct parser *parser, struct value *left, struct value *right) +{ + return (value_from_integer(compare_values(parser, left, right) == 0)); +} + +static struct value +value_compare_ne(struct parser *parser, struct value *left, struct value *right) +{ + return (value_from_integer(compare_values(parser, left, right) != 0)); +} + +static struct value +value_compare_lt(struct parser *parser, struct value *left, struct value *right) +{ + return (value_from_integer(compare_values(parser, left, right) < 0)); +} + +static struct value +value_compare_le(struct parser *parser, struct value *left, struct value *right) +{ + return (value_from_integer(compare_values(parser, left, right) <= 0)); +} + +static struct value +value_compare_gt(struct parser *parser, struct value *left, struct value *right) +{ + return (value_from_integer(compare_values(parser, left, right) > 0)); +} + +static struct value +value_compare_ge(struct parser *parser, struct value *left, struct value *right) +{ + return (value_from_integer(compare_values(parser, left, right) >= 0)); +} + +static struct value +value_add(struct parser *parser, struct value *left, struct value *right) +{ + intmax_t result; + + value_require_integer(parser, left); + value_require_integer(parser, right); + if (add_overflow(left->data.integer, right->data.integer, &result)) { + fail(parser, "overflow"); + } + value_dispose(left); + value_dispose(right); + return (value_from_integer(result)); +} + +static struct value +value_sub(struct parser *parser, struct value *left, struct value *right) +{ + intmax_t result; + + value_require_integer(parser, left); + value_require_integer(parser, right); + if (sub_overflow(left->data.integer, right->data.integer, &result)) { + fail(parser, "overflow"); + } + value_dispose(left); + value_dispose(right); + return (value_from_integer(result)); +} + +static struct value +value_mul(struct parser *parser, struct value *left, struct value *right) +{ + intmax_t result; + + value_require_integer(parser, left); + value_require_integer(parser, right); + if (mul_overflow(left->data.integer, right->data.integer, &result)) { + fail(parser, "overflow"); + } + value_dispose(left); + value_dispose(right); + return (value_from_integer(result)); +} + +static struct value +value_div(struct parser *parser, struct value *left, struct value *right) +{ + intmax_t result; + + value_require_integer(parser, left); + value_require_integer(parser, right); + if (right->data.integer == 0) { + fail(parser, "division by zero"); + } + if (left->data.integer == INTMAX_MIN && right->data.integer == -1) { + fail(parser, "overflow"); + } + result = left->data.integer / right->data.integer; + value_dispose(left); + value_dispose(right); + return (value_from_integer(result)); +} + +static struct value +value_rem(struct parser *parser, struct value *left, struct value *right) +{ + intmax_t result; + + value_require_integer(parser, left); + value_require_integer(parser, right); + if (right->data.integer == 0) { + fail(parser, "division by zero"); + } + result = left->data.integer % right->data.integer; + value_dispose(left); + value_dispose(right); + return (value_from_integer(result)); +} + +static char * +dup_substring(struct parser *parser, const char *text, regoff_t start, regoff_t end) +{ + size_t length; + char *result; + + if (start < 0 || end < start) { + fail(parser, "regular expression returned an invalid capture range"); + } + length = (size_t)(end - start); + result = xmalloc(parser, length + 1); + memcpy(result, text + start, length); + result[length] = '\0'; + return (result); +} + +static struct value +value_match(struct parser *parser, struct value *left, struct value *right) +{ + regex_t regex; + regmatch_t matches[2]; + struct value result; + size_t error_length; + char *error_text; + int regcomp_result; + int regexec_result; + + value_require_string(parser, left); + value_require_string(parser, right); + + regcomp_result = regcomp(®ex, right->data.string, 0); + if (regcomp_result != 0) { + error_length = regerror(regcomp_result, ®ex, NULL, 0); + error_text = xmalloc(parser, error_length); + (void)regerror(regcomp_result, ®ex, error_text, error_length); + fail(parser, "regular expression error: %s", error_text); + } + + regexec_result = regexec(®ex, left->data.string, + (size_t)(sizeof(matches) / sizeof(matches[0])), matches, 0); + if (regexec_result == 0 && matches[0].rm_so == 0) { + if (matches[1].rm_so >= 0) { + result.type = VALUE_STRING; + result.data.string = dup_substring(parser, left->data.string, + matches[1].rm_so, matches[1].rm_eo); + } else { + result = value_from_integer(matches[0].rm_eo); + } + } else if (regexec_result == REG_NOMATCH || + (regexec_result == 0 && matches[0].rm_so != 0)) { + if (regex.re_nsub == 0) { + result = value_from_integer(0); + } else { + result.type = VALUE_STRING; + result.data.string = xstrdup(parser, ""); + } + } else { + error_length = regerror(regexec_result, ®ex, NULL, 0); + error_text = xmalloc(parser, error_length); + (void)regerror(regexec_result, ®ex, error_text, error_length); + regfree(®ex); + fail(parser, "regular expression execution failed: %s", error_text); + } + + regfree(®ex); + value_dispose(left); + value_dispose(right); + return (result); +} + +static const char * +peek_token(const struct parser *parser) +{ + if (parser->index >= parser->argc) { + return (NULL); + } + return (parser->argv[parser->index]); +} + +static bool +match_token(struct parser *parser, const char *token) +{ + const char *current; + + current = peek_token(parser); + if (current == NULL || strcmp(current, token) != 0) { + return (false); + } + parser->index++; + return (true); +} + +static struct value +parse_expression(struct parser *parser) +{ + return (parse_or(parser)); +} + +static struct value +parse_or(struct parser *parser) +{ + struct value left; + struct value right; + + left = parse_and(parser); + while (match_token(parser, "|")) { + right = parse_and(parser); + left = value_or(&left, &right); + } + return (left); +} + +static struct value +parse_and(struct parser *parser) +{ + struct value left; + struct value right; + + left = parse_compare(parser); + while (match_token(parser, "&")) { + right = parse_compare(parser); + left = value_and(&left, &right); + } + return (left); +} + +static struct value +parse_compare(struct parser *parser) +{ + struct value left; + struct value right; + + left = parse_add(parser); + for (;;) { + if (match_token(parser, "=")) { + right = parse_add(parser); + left = value_compare_eq(parser, &left, &right); + } else if (match_token(parser, "!=")) { + right = parse_add(parser); + left = value_compare_ne(parser, &left, &right); + } else if (match_token(parser, ">=")) { + right = parse_add(parser); + left = value_compare_ge(parser, &left, &right); + } else if (match_token(parser, "<=")) { + right = parse_add(parser); + left = value_compare_le(parser, &left, &right); + } else if (match_token(parser, ">")) { + right = parse_add(parser); + left = value_compare_gt(parser, &left, &right); + } else if (match_token(parser, "<")) { + right = parse_add(parser); + left = value_compare_lt(parser, &left, &right); + } else { + break; + } + } + return (left); +} + +static struct value +parse_add(struct parser *parser) +{ + struct value left; + struct value right; + + left = parse_mul(parser); + for (;;) { + if (match_token(parser, "+")) { + right = parse_mul(parser); + left = value_add(parser, &left, &right); + } else if (match_token(parser, "-")) { + right = parse_mul(parser); + left = value_sub(parser, &left, &right); + } else { + break; + } + } + return (left); +} + +static struct value +parse_mul(struct parser *parser) +{ + struct value left; + struct value right; + + left = parse_match_expr(parser); + for (;;) { + if (match_token(parser, "*")) { + right = parse_match_expr(parser); + left = value_mul(parser, &left, &right); + } else if (match_token(parser, "/")) { + right = parse_match_expr(parser); + left = value_div(parser, &left, &right); + } else if (match_token(parser, "%")) { + right = parse_match_expr(parser); + left = value_rem(parser, &left, &right); + } else { + break; + } + } + return (left); +} + +static struct value +parse_match_expr(struct parser *parser) +{ + struct value left; + struct value right; + + left = parse_primary(parser); + while (match_token(parser, ":")) { + right = parse_primary(parser); + left = value_match(parser, &left, &right); + } + return (left); +} + +static struct value +parse_primary(struct parser *parser) +{ + const char *token; + struct value value; + + token = peek_token(parser); + if (token == NULL) { + fail(parser, "syntax error"); + } + if (match_token(parser, "(")) { + value = parse_expression(parser); + if (!match_token(parser, ")")) { + value_dispose(&value); + fail(parser, "syntax error"); + } + return (value); + } + if (is_operator_token(token)) { + fail(parser, "syntax error"); + } + parser->index++; + return (value_from_argument(parser, token)); +} + +static void +write_result(struct parser *parser, const struct value *value) +{ + int rc; + + if (value->type == VALUE_INTEGER) { + rc = fprintf(stdout, "%jd\n", value->data.integer); + } else { + rc = fprintf(stdout, "%s\n", value->data.string); + } + if (rc < 0 || fflush(stdout) == EOF) { + fail_errno(parser, "write failed"); + } +} + +int +main(int argc, char *argv[]) +{ + struct parser parser; + struct value result; + const char *arg; + int i; + + (void)setlocale(LC_ALL, ""); + + parser.program = argv[0] == NULL ? "expr" : argv[0]; + if (strrchr(parser.program, '/') != NULL) { + parser.program = strrchr(parser.program, '/') + 1; + } + parser.compat_mode = false; + parser.index = 0; + parser.argv = NULL; + parser.argc = 0; + + i = 1; + while (i < argc) { + arg = argv[i]; + if (strcmp(arg, "--") == 0) { + i++; + break; + } + if (strcmp(arg, "-e") == 0) { + parser.compat_mode = true; + i++; + continue; + } + if (arg[0] == '-' && arg[1] != '\0') { + usage(); + } + break; + } + + parser.argv = argv + i; + parser.argc = argc - i; + if (parser.argc == 0) { + usage(); + } + + result = parse_expression(&parser); + if (peek_token(&parser) != NULL) { + value_dispose(&result); + fail(&parser, "syntax error"); + } + + write_result(&parser, &result); + i = value_is_zero_or_null(&result) ? 1 : 0; + value_dispose(&result); + return (i); +} diff --git a/corebinutils/expr/tests/test.sh b/corebinutils/expr/tests/test.sh new file mode 100644 index 0000000000..23728d5e3b --- /dev/null +++ b/corebinutils/expr/tests/test.sh @@ -0,0 +1,206 @@ +#!/bin/sh +set -eu + +ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +EXPR_BIN=${EXPR_BIN:-"$ROOT/out/expr"} +TMPDIR=${TMPDIR:-/tmp} +WORKDIR=$(mktemp -d "$TMPDIR/expr-test.XXXXXX") +trap 'rm -rf "$WORKDIR"' EXIT INT TERM + +LC_ALL=C +LANG=C +export LC_ALL LANG + +fail() { + printf '%s\n' "FAIL: $1" >&2 + exit 1 +} + +assert_status() { + name=$1 + expected=$2 + actual=$3 + + [ "$expected" -eq "$actual" ] || fail "$name: expected exit $expected got $actual" +} + +assert_file_match() { + name=$1 + pattern=$2 + actual_file=$3 + + if ! grep -Eq "$pattern" "$actual_file"; then + printf '%s\n' "FAIL: $name" >&2 + printf '%s\n' "--- actual ---" >&2 + cat "$actual_file" >&2 + exit 1 + fi +} + +assert_file_eq() { + name=$1 + expected_text=$2 + actual_file=$3 + expected_file=$WORKDIR/expected.$$ + + printf '%s' "$expected_text" > "$expected_file" + if ! cmp -s "$expected_file" "$actual_file"; then + printf '%s\n' "FAIL: $name" >&2 + printf '%s\n' "--- expected ---" >&2 + od -An -tx1 -v "$expected_file" >&2 + printf '%s\n' "--- actual ---" >&2 + od -An -tx1 -v "$actual_file" >&2 + exit 1 + fi +} + +run_case() { + name=$1 + expected_status=$2 + expected_stdout=$3 + expected_stderr=$4 + shift 4 + + stdout_file=$WORKDIR/stdout + stderr_file=$WORKDIR/stderr + + if "$EXPR_BIN" "$@" >"$stdout_file" 2>"$stderr_file"; then + status=0 + else + status=$? + fi + + assert_status "$name status" "$expected_status" "$status" + assert_file_eq "$name stdout" "$expected_stdout" "$stdout_file" + assert_file_eq "$name stderr" "$expected_stderr" "$stderr_file" +} + +run_case_match_stderr() { + name=$1 + expected_status=$2 + expected_stdout=$3 + stderr_pattern=$4 + shift 4 + + stdout_file=$WORKDIR/stdout + stderr_file=$WORKDIR/stderr + + if "$EXPR_BIN" "$@" >"$stdout_file" 2>"$stderr_file"; then + status=0 + else + status=$? + fi + + assert_status "$name status" "$expected_status" "$status" + assert_file_eq "$name stdout" "$expected_stdout" "$stdout_file" + assert_file_match "$name stderr" "$stderr_pattern" "$stderr_file" +} + +derive_intmax_bounds() { + power=1 + steps=0 + + while next=$("$EXPR_BIN" "$power" "*" "2" 2>/dev/null); do + power=$next + steps=$((steps + 1)) + [ "$steps" -le 256 ] || fail "could not derive intmax bounds" + done + + INTMAX_MAX_VALUE=$("$EXPR_BIN" "(" "$power" "-" "1" ")" "+" "$power") + INTMAX_MIN_VALUE=$("$EXPR_BIN" "0" "-" "1" "-" "$INTMAX_MAX_VALUE") + export INTMAX_MAX_VALUE INTMAX_MIN_VALUE +} + +[ -x "$EXPR_BIN" ] || fail "missing binary: $EXPR_BIN" +derive_intmax_bounds + +run_case "no arguments" 2 "" "usage: expr [-e] [--] expression +" +run_case "invalid option" 2 "" "usage: expr [-e] [--] expression +" "-x" +run_case "integer addition" 0 "7 +" "" "3" "+" "4" +run_case "zero exit status" 1 "0 +" "" "3" "-" "3" +run_case "parentheses precedence" 0 "21 +" "" "(" "3" "+" "4" ")" "*" "3" +run_case "left associative subtraction" 0 "3 +" "" "8" "-" "3" "-" "2" +run_case "left associative division" 0 "2 +" "" "12" "/" "3" "/" "2" +run_case "division by zero" 2 "" "expr: division by zero +" "7" "/" "0" +run_case "remainder by zero" 2 "" "expr: division by zero +" "7" "%" "0" +run_case "type mismatch" 2 "" "expr: not a decimal number: 'abc' +" "abc" "+" "1" +run_case "too large operand" 2 "" "expr: operand too large: '999999999999999999999999999999999999' +" "999999999999999999999999999999999999" "+" "1" +run_case "addition overflow" 2 "" "expr: overflow +" "$INTMAX_MAX_VALUE" "+" "1" +run_case "subtraction overflow" 2 "" "expr: overflow +" "--" "$INTMAX_MIN_VALUE" "-" "1" +run_case "multiplication overflow" 2 "" "expr: overflow +" "$INTMAX_MAX_VALUE" "*" "2" +run_case "division overflow" 2 "" "expr: overflow +" "--" "$INTMAX_MIN_VALUE" "/" "-1" +run_case "string comparison" 0 "1 +" "" "b" ">" "a" +run_case "numeric string comparison" 0 "1 +" "" "--" "09" "=" "9" +run_case "strict plus stays string" 1 "0 +" "" "--" "+7" "=" "7" +run_case "logical or keeps left value" 0 "left +" "" "left" "|" "right" +run_case "logical or returns right" 0 "fallback +" "" "0" "|" "fallback" +run_case "logical and keeps left value" 0 "left +" "" "left" "&" "right" +run_case "logical and returns zero" 1 "0 +" "" "" "&" "right" +run_case "regex length" 0 "6 +" "" "abcdef" ":" "abc.*" +run_case "regex capture" 0 "de +" "" "abcde" ":" "abc\\(..\\)" +run_case "regex anchor" 1 "0 +" "" "abcde" ":" "bc" +run_case "regex capture miss" 1 " +" "" "abcde" ":" "zzz\\(..\\)" +run_case_match_stderr "regex syntax error" 2 "" '^expr: regular expression error: ' "abc" ":" "[[" +run_case "strict negative needs --" 2 "" "usage: expr [-e] [--] expression +" "-1" "+" "2" +run_case "negative with --" 0 "1 +" "" "--" "-1" "+" "2" +run_case "compat still needs -- for leading negative operand" 2 "" "usage: expr [-e] [--] expression +" "-e" "-1" "+" "2" +run_case "compat leading plus" 0 "1 +" "" "-e" " +7" "=" "7" +run_case "compat empty string as zero" 0 "1 +" "" "-e" "" "+" "1" +run_case "compat empty string compares as zero" 0 "1 +" "" "-e" "" "=" "0" +run_case "syntax error" 2 "" "expr: syntax error +" "1" "+" +run_case "trailing token syntax error" 2 "" "expr: syntax error +" "1" "2" +run_case "missing closing parenthesis" 2 "" "expr: syntax error +" "(" "1" "+" "2" + +stdout_file=$WORKDIR/write-fail.stdout +stderr_file=$WORKDIR/write-fail.stderr +if ( + exec 1>&- + "$EXPR_BIN" "write-test" +) >"$stdout_file" 2>"$stderr_file"; then + status=0 +else + status=$? +fi +assert_status "write failure status" 2 "$status" +assert_file_eq "write failure stdout" "" "$stdout_file" +case $(cat "$stderr_file") in + "expr: write failed: "* ) ;; + * ) fail "write failure stderr" ;; +esac + +printf '%s\n' "PASS" |
