Updated flake inputs

This commit is contained in:
2025-01-06 16:26:44 -05:00
parent 92e4ded24e
commit 3857e144e6
77 changed files with 10748 additions and 221 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
result result
target
hardware.nix hardware.nix

286
flake.lock generated
View File

@@ -20,11 +20,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1731959031, "lastModified": 1736102453,
"narHash": "sha256-TGcvIjftziC1CjuiHCzrYDwmOoSFYIhdiKmLetzB5L0=", "narHash": "sha256-5qb4kb7Xbt8jJFL/oDqOor9Z2+E+A+ql3PiyDvsfWZ0=",
"owner": "hyprwm", "owner": "hyprwm",
"repo": "aquamarine", "repo": "aquamarine",
"rev": "4468981c1c50999f315baa1508f0e53c4ee70c52", "rev": "4846091641f3be0ad7542086d52769bb7932bde6",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -107,11 +107,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1728330715, "lastModified": 1735644329,
"narHash": "sha256-xRJ2nPOXb//u1jaBnDP56M7v5ldavjbtR6lfGqSvcKg=", "narHash": "sha256-tO3HrHriyLvipc4xr+Ewtdlo7wM1OjXNjlWRgmM7peY=",
"owner": "numtide", "owner": "numtide",
"repo": "devshell", "repo": "devshell",
"rev": "dd6b80932022cea34a019e2bb32f6fa9e494dfef", "rev": "f7795ede5b02664b57035b3b757876703e2c3eac",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -125,11 +125,11 @@
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
}, },
"locked": { "locked": {
"lastModified": 1734088167, "lastModified": 1736165297,
"narHash": "sha256-OIitVU+IstPbX/NWn2jLF+/sT9dVKcO2FKeRAzlyX6c=", "narHash": "sha256-OT+sF4eNDFN/OdyUfIQwyp28+CFQL7PAdWn0wGU7F0U=",
"owner": "nix-community", "owner": "nix-community",
"repo": "disko", "repo": "disko",
"rev": "d32f2d1750d61a476a236526b725ec5a32e16342", "rev": "76816af65d5294761636a838917e335992a52e0c",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -208,11 +208,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1733312601, "lastModified": 1736143030,
"narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=", "narHash": "sha256-+hu54pAoLDEZT9pjHlqL9DNzWz0NbUn8NEAHP7PQPzU=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "flake-parts", "repo": "flake-parts",
"rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9", "rev": "b905f6fc23a9051a6e1b741e1438dbfc0634c6de",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -286,18 +286,45 @@
"nixpkgs": [ "nixpkgs": [
"nixvim", "nixvim",
"nixpkgs" "nixpkgs"
]
},
"locked": {
"lastModified": 1735882644,
"narHash": "sha256-3FZAG+pGt3OElQjesCAWeMkQ7C/nB1oTHLRQ8ceP110=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "a5a961387e75ae44cc20f0a57ae463da5e959656",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"git-hooks_2": {
"inputs": {
"flake-compat": [
"stylix",
"flake-compat"
],
"gitignore": "gitignore_3",
"nixpkgs": [
"stylix",
"nixpkgs"
], ],
"nixpkgs-stable": [ "nixpkgs-stable": [
"nixvim", "stylix",
"git-hooks",
"nixpkgs" "nixpkgs"
] ]
}, },
"locked": { "locked": {
"lastModified": 1733318908, "lastModified": 1731363552,
"narHash": "sha256-SVQVsbafSM1dJ4fpgyBqLZ+Lft+jcQuMtEL3lQWx2Sk=", "narHash": "sha256-vFta1uHnD29VUY4HJOO/D6p6rxyObnf+InnSMT4jlMU=",
"owner": "cachix", "owner": "cachix",
"repo": "git-hooks.nix", "repo": "git-hooks.nix",
"rev": "6f4e2a2112050951a314d2733a994fbab94864c6", "rev": "cd1af27aa85026ac759d5d3fccf650abe7e1bbf0",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -350,6 +377,28 @@
"type": "github" "type": "github"
} }
}, },
"gitignore_3": {
"inputs": {
"nixpkgs": [
"stylix",
"git-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"gnome-shell": { "gnome-shell": {
"flake": false, "flake": false,
"locked": { "locked": {
@@ -374,11 +423,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1734093295, "lastModified": 1736089250,
"narHash": "sha256-hSwgGpcZtdDsk1dnzA0xj5cNaHgN9A99hRF/mxMtwS4=", "narHash": "sha256-/LPWMiiJGPHGd7ZYEgmbE2da4zvBW0acmshUjYC3WG4=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "66c5d8b62818ec4c1edb3e941f55ef78df8141a8", "rev": "172b91bfb2b7f5c4a8c6ceac29fd53a01ef07196",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -395,11 +444,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1733484277, "lastModified": 1736089250,
"narHash": "sha256-i5ay20XsvpW91N4URET/nOc0VQWOAd4c4vbqYtcH8Rc=", "narHash": "sha256-/LPWMiiJGPHGd7ZYEgmbE2da4zvBW0acmshUjYC3WG4=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "d00c6f6d0ad16d598bf7e2956f52c1d9d5de3c3a", "rev": "172b91bfb2b7f5c4a8c6ceac29fd53a01ef07196",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -416,11 +465,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1733085484, "lastModified": 1735774425,
"narHash": "sha256-dVmNuUajnU18oHzBQWZm1BQtANCHaqNuxTHZQ+GN0r8=", "narHash": "sha256-C73gLFnEh8ZI0uDijUgCDWCd21T6I6tsaWgIBHcfAXg=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "c1fee8d4a60b89cae12b288ba9dbc608ff298163", "rev": "5f6aa268e419d053c3d5025da740e390b12ac936",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -463,11 +512,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1728669738, "lastModified": 1734906540,
"narHash": "sha256-EDNAU9AYcx8OupUzbTbWE1d3HYdeG0wO6Msg3iL1muk=", "narHash": "sha256-vQ/L9hZFezC0LquLo4TWXkyniWtYBlFHAKIsDc7PYJE=",
"owner": "hyprwm", "owner": "hyprwm",
"repo": "hyprcursor", "repo": "hyprcursor",
"rev": "0264e698149fcb857a66a53018157b41f8d97bb0", "rev": "69270ba8f057d55b0e6c2dca0e165d652856e613",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -492,11 +541,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1733248371, "lastModified": 1736115290,
"narHash": "sha256-FFLJzFTyNhS7tBEEECx0B8Ye/bpmxhFVEKlECgMLc6c=", "narHash": "sha256-Jcn6yAzfUMcxy3tN/iZRbi/QgrYm7XLyVRl9g/nbUl4=",
"owner": "hyprwm", "owner": "hyprwm",
"repo": "hyprgraphics", "repo": "hyprgraphics",
"rev": "cc95e5babc6065bc3ab4cd195429a9900836ef13", "rev": "52202272d89da32a9f866c0d10305a5e3d954c50",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -521,11 +570,11 @@
"xdph": "xdph" "xdph": "xdph"
}, },
"locked": { "locked": {
"lastModified": 1734121737, "lastModified": 1736191857,
"narHash": "sha256-aVI7Oic+PgEX4T6PRH572L2fMxGqnWM9K9xSPNjoV9Y=", "narHash": "sha256-2ADoC5iTawhEmEuDrl5z+gLbLHEsYqdjwnlF5Y5MZmA=",
"ref": "refs/heads/main", "ref": "refs/heads/main",
"rev": "452a7e6905de61f076065ef4c294dc62f86327ae", "rev": "b9f110ef8726fcba2b4ee69856027731e73003a5",
"revCount": 5537, "revCount": 5638,
"submodules": true, "submodules": true,
"type": "git", "type": "git",
"url": "https://github.com/hyprwm/Hyprland" "url": "https://github.com/hyprwm/Hyprland"
@@ -548,11 +597,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1728345020, "lastModified": 1735774328,
"narHash": "sha256-xGbkc7U/Roe0/Cv3iKlzijIaFBNguasI31ynL2IlEoM=", "narHash": "sha256-vIRwLS9w+N99EU1aJ+XNOU6mJTxrUBa31i1r82l0V7s=",
"owner": "hyprwm", "owner": "hyprwm",
"repo": "hyprland-protocols", "repo": "hyprland-protocols",
"rev": "a7c183800e74f337753de186522b9017a07a8cee", "rev": "e3b6af97ddcfaafbda8e2828c719a5af84f662cb",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -577,11 +626,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1733472316, "lastModified": 1736114838,
"narHash": "sha256-PvXiFLIExJEJj+goLbIuXLTN5CSDSAUsAfiYSdbbWg0=", "narHash": "sha256-FxbuGQExtN37ToWYnGmO6weOYN6WPHN/RAqbr7gNPek=",
"owner": "hyprwm", "owner": "hyprwm",
"repo": "hyprland-qtutils", "repo": "hyprland-qtutils",
"rev": "969427419276c7ee170301ef1ebe0f68eb6eb2e2", "rev": "6997fe382dcf396704227d2b98ffdd5066da6959",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -606,11 +655,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1728168612, "lastModified": 1735393019,
"narHash": "sha256-AnB1KfiXINmuiW7BALYrKqcjCnsLZPifhb/7BsfPbns=", "narHash": "sha256-NPpqA8rtmDLsEmZOmz+qR67zsB6Y503Jnv+nSFLKJZ8=",
"owner": "hyprwm", "owner": "hyprwm",
"repo": "hyprlang", "repo": "hyprlang",
"rev": "f054f2e44d6a0b74607a6bc0f52dba337a3db38e", "rev": "55608efdaa387af7bfdc0eddb404c409958efa43",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -627,11 +676,11 @@
"systems": "systems_2" "systems": "systems_2"
}, },
"locked": { "locked": {
"lastModified": 1732810179, "lastModified": 1735584197,
"narHash": "sha256-J39wjWN4+dxjwf8Fw5yv1aVcbkOg7oQi42zVY4ZxJMU=", "narHash": "sha256-B1PqiHp/jmDVXVrvyh/eu2KP3LCyi1JL0h3vuy/wVnM=",
"owner": "hyprwm", "owner": "hyprwm",
"repo": "hyprpicker", "repo": "hyprpicker",
"rev": "d26cb2f4396e4fd3477f5f12131c87c2f5002ab7", "rev": "444c40e5e3dc4058a6a762ba5e73ada6d6469055",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -652,11 +701,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1732288281, "lastModified": 1736164519,
"narHash": "sha256-XTU9B53IjGeJiJ7LstOhuxcRjCOFkQFl01H78sT9Lg4=", "narHash": "sha256-1LimBKvDpBbeX+qW7T240WEyw+DBVpDotZB4JYm8Aps=",
"owner": "hyprwm", "owner": "hyprwm",
"repo": "hyprutils", "repo": "hyprutils",
"rev": "b26f33cc1c8a7fd5076e19e2cce3f062dca6351c", "rev": "3c895da64b0eb19870142196fa48c07090b441c4",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -677,11 +726,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1727300645, "lastModified": 1733502241,
"narHash": "sha256-OvAtVLaSRPnbXzOwlR1fVqCXR7i+ICRX3aPMCdIiv+c=", "narHash": "sha256-KAUNC4Dgq8WQjYov5auBw/usaHixhacvb7cRDd0AG/k=",
"owner": "hyprwm", "owner": "hyprwm",
"repo": "hyprutils", "repo": "hyprutils",
"rev": "3f5293432b6dc6a99f26aca2eba3876d2660665c", "rev": "104117aed6dd68561be38b50f218190aa47f2cd8",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -702,11 +751,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1726874836, "lastModified": 1735493474,
"narHash": "sha256-VKR0sf0PSNCB0wPHVKSAn41mCNVCnegWmgkrneKDhHM=", "narHash": "sha256-fktzv4NaqKm94VAkAoVqO/nqQlw+X0/tJJNAeCSfzK4=",
"owner": "hyprwm", "owner": "hyprwm",
"repo": "hyprwayland-scanner", "repo": "hyprwayland-scanner",
"rev": "500c81a9e1a76760371049a8d99e008ea77aa59e", "rev": "de913476b59ee88685fdc018e77b8f6637a2ae0b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -742,11 +791,11 @@
}, },
"impermanence": { "impermanence": {
"locked": { "locked": {
"lastModified": 1731242966, "lastModified": 1734945620,
"narHash": "sha256-B3C3JLbGw0FtLSWCjBxU961gLNv+BOOBC6WvstKLYMw=", "narHash": "sha256-olIfsfJK4/GFmPH8mXMmBDAkzVQ1TWJmeGT3wBGfQPY=",
"owner": "nix-community", "owner": "nix-community",
"repo": "impermanence", "repo": "impermanence",
"rev": "3ed3f0eaae9fcc0a8331e77e9319c8a4abd8a71a", "rev": "d000479f4f41390ff7cf9204979660ad5dd16176",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -791,11 +840,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1733570843, "lastModified": 1736085891,
"narHash": "sha256-sQJAxY1TYWD1UyibN/FnN97paTFuwBw3Vp3DNCyKsMk=", "narHash": "sha256-bTl9fcUo767VaSx4Q5kFhwiDpFQhBKna7lNbGsqCQiA=",
"owner": "lnl7", "owner": "lnl7",
"repo": "nix-darwin", "repo": "nix-darwin",
"rev": "a35b08d09efda83625bef267eb24347b446c80b8", "rev": "ba9b3173b0f642ada42b78fb9dfc37ca82266f6c",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -806,11 +855,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1733749988, "lastModified": 1735915915,
"narHash": "sha256-+5qdtgXceqhK5ZR1YbP1fAUsweBIrhL38726oIEAtDs=", "narHash": "sha256-Q4HuFAvoKAIiTRZTUxJ0ZXeTC7lLfC9/dggGHNXNlCw=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "bc27f0fde01ce4e1bfec1ab122d72b7380278e68", "rev": "a27871180d30ebee8aa6b11bf7fef8a52f024733",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -820,22 +869,6 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs-stable": {
"locked": {
"lastModified": 1730741070,
"narHash": "sha256-edm8WG19kWozJ/GqyYx2VjW99EdhjKwbY3ZwdlPAAlo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d063c1dd113c91ab27959ba540c0d9753409edf3",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1712163089, "lastModified": 1712163089,
@@ -854,11 +887,11 @@
}, },
"nixpkgs_3": { "nixpkgs_3": {
"locked": { "locked": {
"lastModified": 1733392399, "lastModified": 1736012469,
"narHash": "sha256-kEsTJTUQfQFIJOcLYFt/RvNxIK653ZkTBIs4DG+cBns=", "narHash": "sha256-/qlNWm/IEVVH7GfgAIyP6EsVZI6zjAx1cV5zNyrs+rI=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "d0797a04b81caeae77bcff10a9dde78bc17f5661", "rev": "8f3e1f807051e32d8c95cd12b9b421623850a34d",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -870,11 +903,11 @@
}, },
"nixpkgs_4": { "nixpkgs_4": {
"locked": { "locked": {
"lastModified": 1727122398, "lastModified": 1734119587,
"narHash": "sha256-o8VBeCWHBxGd4kVMceIayf5GApqTavJbTa44Xcg5Rrk=", "narHash": "sha256-AKU6qqskl0yf2+JdRdD0cfxX4b9x3KKV5RqA6wijmPM=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "30439d93eb8b19861ccbe3e581abf97bdc91b093", "rev": "3566ab7246670a43abd2ffa913cc62dad9cdf7d5",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -886,11 +919,11 @@
}, },
"nixpkgs_5": { "nixpkgs_5": {
"locked": { "locked": {
"lastModified": 1733940404, "lastModified": 1736012469,
"narHash": "sha256-Pj39hSoUA86ZePPF/UXiYHHM7hMIkios8TYG29kQT4g=", "narHash": "sha256-/qlNWm/IEVVH7GfgAIyP6EsVZI6zjAx1cV5zNyrs+rI=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "5d67ea6b4b63378b9c13be21e2ec9d1afc921713", "rev": "8f3e1f807051e32d8c95cd12b9b421623850a34d",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -902,16 +935,16 @@
}, },
"nixpkgs_6": { "nixpkgs_6": {
"locked": { "locked": {
"lastModified": 1732238832, "lastModified": 1735648875,
"narHash": "sha256-sQxuJm8rHY20xq6Ah+GwIUkF95tWjGRd1X8xF+Pkk38=", "narHash": "sha256-fQ4k/hyQiH9RRPznztsA9kbcDajvwV1sRm01el6Sr3c=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "8edf06bea5bcbee082df1b7369ff973b91618b8d", "rev": "47e29c20abef74c45322eca25ca1550cdf5c3b50",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "nixpkgs-unstable", "ref": "nixos-unstable",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
@@ -931,11 +964,11 @@
"treefmt-nix": "treefmt-nix" "treefmt-nix": "treefmt-nix"
}, },
"locked": { "locked": {
"lastModified": 1734103614, "lastModified": 1736157655,
"narHash": "sha256-H5JN0fajkKZLir/GN6QHmLsR3cW+/EIOR+W/VmwHKfI=", "narHash": "sha256-/ggXMK8Q/rN94kaaSHPtEcf4SPKgPXfzSbDgAR6Odzs=",
"owner": "nix-community", "owner": "nix-community",
"repo": "nixvim", "repo": "nixvim",
"rev": "c181014422fa9261db06fc9b5ecbf67f42c30ec3", "rev": "31139e0605fd886d981e0a197e30ceac4b859d6e",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -954,11 +987,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1733411491, "lastModified": 1735854821,
"narHash": "sha256-315rJ7O9cOllPDaFscnJhcMleORHbxon0Kq9LAKJ5p4=", "narHash": "sha256-Iv59gMDZajNfezTO0Fw6LHE7uKAShxbvMidmZREit7c=",
"owner": "NuschtOS", "owner": "NuschtOS",
"repo": "search", "repo": "search",
"rev": "68e9fad70d95d08156cf10a030bd39487bed8ffe", "rev": "836908e3bddd837ae0f13e215dd48767aee355f0",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -974,15 +1007,14 @@
"nixpkgs": [ "nixpkgs": [
"hyprland", "hyprland",
"nixpkgs" "nixpkgs"
], ]
"nixpkgs-stable": "nixpkgs-stable"
}, },
"locked": { "locked": {
"lastModified": 1733318908, "lastModified": 1735882644,
"narHash": "sha256-SVQVsbafSM1dJ4fpgyBqLZ+Lft+jcQuMtEL3lQWx2Sk=", "narHash": "sha256-3FZAG+pGt3OElQjesCAWeMkQ7C/nB1oTHLRQ8ceP110=",
"owner": "cachix", "owner": "cachix",
"repo": "git-hooks.nix", "repo": "git-hooks.nix",
"rev": "6f4e2a2112050951a314d2733a994fbab94864c6", "rev": "a5a961387e75ae44cc20f0a57ae463da5e959656",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -1013,11 +1045,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1734063436, "lastModified": 1736136999,
"narHash": "sha256-wE1sIAnsjWbyXXjwC/+oxSFXFDCROiwLY1pSQ7pU9js=", "narHash": "sha256-bI3pSXx4fihlWNi4b8+Kp04RkDhctEgGdJuNQw/2O88=",
"owner": "gerg-l", "owner": "gerg-l",
"repo": "spicetify-nix", "repo": "spicetify-nix",
"rev": "7981c1e87aa1adeec524524db52f75bf6598fb55", "rev": "046dc4cc0bcf460cafbfd3fe26fdc584f5b0fb80",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -1034,20 +1066,22 @@
"base16-vim": "base16-vim", "base16-vim": "base16-vim",
"flake-compat": "flake-compat_4", "flake-compat": "flake-compat_4",
"flake-utils": "flake-utils_2", "flake-utils": "flake-utils_2",
"git-hooks": "git-hooks_2",
"gnome-shell": "gnome-shell", "gnome-shell": "gnome-shell",
"home-manager": "home-manager_3", "home-manager": "home-manager_3",
"nixpkgs": "nixpkgs_6", "nixpkgs": "nixpkgs_6",
"systems": "systems_4", "systems": "systems_4",
"tinted-foot": "tinted-foot", "tinted-foot": "tinted-foot",
"tinted-kitty": "tinted-kitty", "tinted-kitty": "tinted-kitty",
"tinted-tmux": "tinted-tmux" "tinted-tmux": "tinted-tmux",
"tinted-zed": "tinted-zed"
}, },
"locked": { "locked": {
"lastModified": 1734110168, "lastModified": 1736179037,
"narHash": "sha256-Q0eeLYn45ErXlqGQyXmLLHGe1mqnUiK0Y9wZRa1SNFI=", "narHash": "sha256-uhLRE3x5TrFeQm1waBz6ecqa2EHilFM4jLxYbOJPGrU=",
"owner": "danth", "owner": "danth",
"repo": "stylix", "repo": "stylix",
"rev": "a9e3779949925ef22f5a215c5f49cf520dea30b1", "rev": "a6b53aa677ab9bd9098abbdc47924854c76c3eb1",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -1166,6 +1200,22 @@
"type": "github" "type": "github"
} }
}, },
"tinted-zed": {
"flake": false,
"locked": {
"lastModified": 1725758778,
"narHash": "sha256-8P1b6mJWyYcu36WRlSVbuj575QWIFZALZMTg5ID/sM4=",
"owner": "tinted-theming",
"repo": "base16-zed",
"rev": "122c9e5c0e6f27211361a04fae92df97940eccf9",
"type": "github"
},
"original": {
"owner": "tinted-theming",
"repo": "base16-zed",
"type": "github"
}
},
"treefmt-nix": { "treefmt-nix": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@@ -1174,11 +1224,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1733440889, "lastModified": 1736115332,
"narHash": "sha256-qKL3vjO+IXFQ0nTinFDqNq/sbbnnS5bMI1y0xX215fU=", "narHash": "sha256-FBG9d7e0BTFfxVdw4b5EmNll2Mv7hfRc54hbB4LrKko=",
"owner": "numtide", "owner": "numtide",
"repo": "treefmt-nix", "repo": "treefmt-nix",
"rev": "50862ba6a8a0255b87377b9d2d4565e96f29b410", "rev": "1788ca5acd4b542b923d4757d4cfe4183cc6a92d",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -1215,11 +1265,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1733157064, "lastModified": 1734907020,
"narHash": "sha256-NetqJHAN4bbZDQADvpep+wXk2AbMZ2bN6tINz8Kpz6M=", "narHash": "sha256-p6HxwpRKVl1KIiY5xrJdjcEeK3pbmc///UOyV6QER+w=",
"owner": "hyprwm", "owner": "hyprwm",
"repo": "xdg-desktop-portal-hyprland", "repo": "xdg-desktop-portal-hyprland",
"rev": "fd85ef39369f95eed67fdf3f025e86916edeea2f", "rev": "d7f18dda5e511749fa1511185db3536208fb1a63",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -142,6 +142,9 @@
specialArgs = { specialArgs = {
inherit self inputs username; inherit self inputs username;
host = "oganesson"; host = "oganesson";
overlays = [
(import ./overlay/overlay.nix { root = self; })
];
}; };
inherit system; inherit system;
pkgs = import nixpkgs { pkgs = import nixpkgs {

View File

@@ -1,4 +1,31 @@
{ pkgs, username, ... }: { { pkgs, username, ... }:
let rsh = pkgs.rustPlatform.buildRustPackage {
pname = "rsh";
version = "0.0.1";
buildType = "debug";
src = ../../overlay/pkgs/rsh/rsh;
cargoHash = "sha256-8Lb7AohSah2A7pcoT2JPDgza0LfIyD897yj4QHNklDw=";
cargoLock = {
lockFile = ../../overlay/pkgs/rsh/rsh/Cargo.lock;
};
buildInputs = [ ];
meta = {
description = "Modern shell scripting";
homepage = "https://github.com/pagedMov/rsh";
license = pkgs.lib.licenses.gpl3;
maintainers = with pkgs.lib.maintainers; [ pagedMov ];
};
passthru = {
shellPath = "/bin/rsh";
};
};
in
{
imports = [ ./hardware.nix ]; imports = [ ./hardware.nix ];
# My module options # My module options
@@ -8,6 +35,7 @@
sddmConfig.enable = false; sddmConfig.enable = false;
stylixConfig.enable = true; stylixConfig.enable = true;
nixSettings.enable = true; nixSettings.enable = true;
#consoleSettings.enable = true;
}; };
hardwareCfg = { hardwareCfg = {
networkModule.enable = true; networkModule.enable = true;
@@ -25,7 +53,7 @@
environment = { environment = {
variables = { PATH = "${pkgs.clang-tools}/bin:$PATH"; }; variables = { PATH = "${pkgs.clang-tools}/bin:$PATH"; };
shells = with pkgs; [ zsh bash ]; shells = [ rsh pkgs.zsh pkgs.bash ];
}; };
users = { users = {
@@ -36,7 +64,7 @@
isNormalUser = true; isNormalUser = true;
initialPassword = "1234"; initialPassword = "1234";
shell = pkgs.zsh; shell = pkgs.zsh;
extraGroups = [ "wheel" "persist" "libvirtd" ]; extraGroups = [ "input" "wheel" "persist" "libvirtd" ];
}; };
}; };
}; };

View File

@@ -1,7 +1,7 @@
{ lib, self, config, host, pkgs, ... }: { lib, self, config, host, pkgs, ... }:
let let
scheme = "tokyo-night-dark"; scheme = "ayu-dark";
wallpaper = "${self}/assets/wallpapers/dark-waves.jpg"; wallpaper = "${self}/assets/wallpapers/dark-waves.jpg";
server = (host == "xenon"); server = (host == "xenon");
in { in {
@@ -16,10 +16,11 @@ in {
image = wallpaper; image = wallpaper;
polarity = "dark"; polarity = "dark";
autoEnable = true; autoEnable = true;
opacity.terminal = 0.5; opacity.terminal = 1.0;
targets = { targets = {
waybar.enable = false; waybar.enable = false;
btop.enable = false; btop.enable = false;
nixvim.enable = false;
nixvim.transparentBackground = { nixvim.transparentBackground = {
main = false; main = false;
signColumn = false; signColumn = false;

View File

@@ -49,6 +49,16 @@ in {
wf-recorder wf-recorder
toilet toilet
vkbasalt vkbasalt
firefox
dust
porsmo
rustup
w3m
neovide
python3
ghostty
fd
delta
] ++ scripts; ] ++ scripts;
}; };
} }

View File

@@ -4,7 +4,9 @@
config = lib.mkIf config.movOpts.envConfig.zshConfig.shellAliases.enable { config = lib.mkIf config.movOpts.envConfig.zshConfig.shellAliases.enable {
programs.zsh = { programs.zsh = {
shellAliases = { shellAliases = {
grep = "grep --color=auto"; grep = "rg";
find = "fd";
cat = "bat";
yazi = "y"; yazi = "y";
mv = "mv -v"; mv = "mv -v";
cp = "cp -vr"; cp = "cp -vr";
@@ -28,6 +30,7 @@
gpush = "gitpush_sfx"; gpush = "gitpush_sfx";
gpull = "gitpull_sfx"; gpull = "gitpull_sfx";
greb = "gitrebase_sfx"; greb = "gitrebase_sfx";
rsh = "$HOME/Coding/projects/rust/rsh/target/debug/rsh";
}; };
}; };
}; };

View File

@@ -37,7 +37,12 @@ in
${shellsound} ${sndpath}/nvim.wav ${shellsound} ${sndpath}/nvim.wav
command nvim "$@" command nvim "$@"
} }
neovide() {
${shellsound} ${sndpath}/nvim.wav
command neovide "$@"
}
alias vi="nvim" alias vi="nvim"
alias vide="neovide"
kitty_theme() { kitty_theme() {
if [ $TERM = "xterm-kitty" ]; then if [ $TERM = "xterm-kitty" ]; then
if [ -n "$SSH_CONNECTION" ]; then if [ -n "$SSH_CONNECTION" ]; then
@@ -135,6 +140,10 @@ in
touch $HOME/.zsh_history touch $HOME/.zsh_history
chmod 600 $HOME/.zsh_history chmod 600 $HOME/.zsh_history
fi fi
if [ "$TERM" = "linux" ]; then
echo -en "\e]P0101010"
setfont ter-v32b
fi
setopt APPEND_HISTORY # Append history to the history file (don't overwrite) setopt APPEND_HISTORY # Append history to the history file (don't overwrite)
setopt INC_APPEND_HISTORY # Append to the history file incrementally setopt INC_APPEND_HISTORY # Append to the history file incrementally
setopt SHARE_HISTORY # Share history between all zsh sessions setopt SHARE_HISTORY # Share history between all zsh sessions

View File

@@ -13,20 +13,12 @@
}; };
userEmail = "kylerclay@proton.me"; userEmail = "kylerclay@proton.me";
userName = "${username}"; userName = "${username}";
diff-so-fancy = {
enable = true;
markEmptyLines = false;
stripLeadingSymbols = false;
};
extraConfig = { extraConfig = {
color.diff = { core.pager = "delta";
# meta = "black yellow bold"; interactive.diffFilter = "delta --color-only";
# frag = "white blue bold"; delta.navigate = "true";
old = "#A9B1D6 #301A1F"; delta.dark = "true";
new = "#A9B1D6 #12261E"; merge.conflictstyle = "zdiff3";
# plain = "normal";
# whitespace = "reverse red";
};
}; };
}; };
}; };

View File

@@ -6,6 +6,11 @@
config = lib.mkIf config.movOpts.programConfigs.kittyConfig.enable { config = lib.mkIf config.movOpts.programConfigs.kittyConfig.enable {
programs.kitty = { programs.kitty = {
enable = true; enable = true;
font = {
package = lib.mkForce pkgs.fira-code;
name = lib.mkForce "Fira Code";
size = lib.mkForce 20;
};
settings = { settings = {
confirm_os_window_close = 0; confirm_os_window_close = 0;

View File

@@ -15,6 +15,12 @@
desc = desc =
"Load previous session window settings for the opened file (folds, cursor pos, etc)"; "Load previous session window settings for the opened file (folds, cursor pos, etc)";
} }
{
command = "silent! FloatermNew --name=def_term --width=0.8 --height=0.8 --wintype=topright --silent";
event = [ "VimEnter" ];
pattern = [ "*" ];
desc = "Start the floaterm window";
}
]; ];
}; };
} }

View File

@@ -6,24 +6,14 @@
key = "!fs"; key = "!fs";
mode = "n"; mode = "n";
} }
{
action = "<cmd>UndotreeToggle<CR>"; # select entire document
key = "!t";
mode = "n";
}
{ {
action = "gg<S-V>G"; # select entire document action = "gg<S-V>G"; # select entire document
key = "<C-a>"; key = "!a";
mode = "n";
}
{
action = "<cmd>ChatGPTEditWithInstructions<CR>";
key = "!egpt";
mode = "n";
}
{
action = "<cmd>ChatGPTCompleteCode<CR>";
key = "!cgpt";
mode = "n";
}
{
action = "<cmd>ChatGPT<CR>";
key = "!gpt";
mode = "n"; mode = "n";
} }
{ {
@@ -31,11 +21,6 @@
key = "!ca"; key = "!ca";
mode = "n"; mode = "n";
} }
{
action = "<cmd>Telescope<CR>";
key = "!t";
mode = "n";
}
{ {
action = "<cmd>lua vim.lsp.buf.format()<CR>"; action = "<cmd>lua vim.lsp.buf.format()<CR>";
key = "!fmt"; key = "!fmt";
@@ -57,20 +42,15 @@
mode = "n"; mode = "n";
} }
{ {
action = "<cmd>FloatermToggle shadeterm<CR>"; action = "<cmd>FloatermToggle def_term<CR>";
key = "<F2>"; key = "<F2>";
mode = "n"; mode = [ "n" "t" ];
} }
{ {
action = "<cmd>NvimTreeToggle<CR>"; action = "<cmd>NvimTreeToggle<CR>";
key = "<F3>"; key = "<F3>";
mode = "n"; mode = "n";
} }
{
action = "<cmd>FloatermToggle shadeterm<CR>";
key = "<F2>";
mode = "t";
}
{ {
action = "<cmd>COQnow<CR>"; action = "<cmd>COQnow<CR>";
key = "!cq"; key = "!cq";

View File

@@ -5,6 +5,7 @@ in {
programs.nixvim = { programs.nixvim = {
colorschemes.base16 = { colorschemes.base16 = {
enable = true; enable = true;
colorscheme = "ayu-dark";
#colorscheme = { #colorscheme = {
# base00 = "#${scheme.base00}"; # base00 = "#${scheme.base00}";
# base01 = "#${scheme.base01}"; # base01 = "#${scheme.base01}";
@@ -51,8 +52,10 @@ in {
vim.opt.linebreak = true vim.opt.linebreak = true
vim.opt.textwidth = 0 vim.opt.textwidth = 0
vim.opt.breakat = " \t!@*-+;:,./?" vim.opt.breakat = " \t!@*-+;:,./?"
vim.opt.guifont = "Fira Code:h18"
vim.g.mapleader = "!" vim.g.mapleader = "!"
vim.g.rust_recommended_style = 0
''; '';
}; };
} }

View File

@@ -0,0 +1,16 @@
{
programs.nixvim = {
plugins.airline = {
enable = true;
settings = {
left_sep = "";
right_sep = "";
powerline_fonts = 1;
theme = "dark";
section_x = "";
section_y = "";
skip_empty_sections = 1;
};
};
};
}

View File

@@ -12,12 +12,15 @@
./lsp.nix ./lsp.nix
./rustaceanvim.nix ./rustaceanvim.nix
./fidget.nix ./fidget.nix
./lualine.nix # ./lualine.nix
./airline.nix
./nvim-lightbulb.nix ./nvim-lightbulb.nix
./neocord.nix ./neocord.nix
./plugins.nix ./plugins.nix
./nvim-tree.nix ./nvim-tree.nix
./telescope.nix ./telescope.nix
./indent-blankline.nix
./gitsigns.nix
./extra_plugins.nix ./extra_plugins.nix
]; ];
} }

View File

@@ -0,0 +1,14 @@
{
programs.nixvim = {
plugins.gitsigns = {
enable = true;
settings.signs = {
add.text = "";
change.text = "/";
delete.text = "-";
topdelete.text = "-";
changedelete.text = "\\";
};
};
};
}

View File

@@ -1,16 +1,16 @@
{ pkgs, ... }: { { pkgs, ... }: {
programs.nixvim = { programs.nixvim = {
extraPlugins = [ #extraPlugins = [
(pkgs.vimUtils.buildVimPlugin { #(pkgs.vimUtils.buildVimPlugin {
name = "haskell-tools.nvim"; #name = "haskell-tools.nvim";
src = pkgs.fetchFromGitHub { #src = pkgs.fetchFromGitHub {
owner = "mrcjkb"; #owner = "mrcjkb";
repo = "haskell-tools.nvim"; #repo = "haskell-tools.nvim";
rev = "39c4ced6f1bff1abc8d4df5027efd11ac38c6e6c"; #rev = "39c4ced6f1bff1abc8d4df5027efd11ac38c6e6c";
hash = "sha256-f+M35EwAlHwjJ2Xs2u9FLnyH0FJT22D0LLShDXCbEEs="; #hash = "sha256-f+M35EwAlHwjJ2Xs2u9FLnyH0FJT22D0LLShDXCbEEs=";
}; #};
}) #})
]; #];
plugins = { haskell-scope-highlighting.enable = true; }; #plugins = { haskell-scope-highlighting.enable = true; };
}; };
} }

View File

@@ -0,0 +1,10 @@
{
programs.nixvim = {
plugins.indent-blankline = {
enable = true;
#settings = {
#indent.char = "│";
#};
};
};
}

View File

@@ -1,7 +1,7 @@
{ {
programs.nixvim = { programs.nixvim = {
plugins.lualine = { plugins.lualine = {
enable = true; enable = false;
settings = { settings = {
options = { options = {
icons_enabled = true; icons_enabled = true;

View File

@@ -7,11 +7,11 @@
}; };
nix.enable = true; nix.enable = true;
endwise.enable = true; endwise.enable = true;
undotree.enable = true;
firenvim.enable = true; firenvim.enable = true;
helpview.enable = true; helpview.enable = true;
floaterm.enable = true; floaterm.enable = true;
fugitive.enable = true; fugitive.enable = true;
gitsigns.enable = true;
indent-blankline.enable = true; indent-blankline.enable = true;
lastplace.enable = true; lastplace.enable = true;
markdown-preview.enable = true; markdown-preview.enable = true;

View File

@@ -6,6 +6,7 @@
settings.diagnostics_warn_symbol = ""; settings.diagnostics_warn_symbol = "";
settings.diagnostics_hint_symbol = ""; settings.diagnostics_hint_symbol = "";
settings.diagnostics_info_symbol = ""; settings.diagnostics_info_symbol = "";
settings.character = "";
}; };
}; };
} }

View File

@@ -2,19 +2,17 @@
programs.nixvim = { programs.nixvim = {
plugins.vim-matchup = { plugins.vim-matchup = {
enable = true; enable = true;
enableSurround = true; settings = {
textObj.enable = true; surround_enabled = 1;
motion = { text_obj_enabled = 1;
motion_enabled = 1;
motion_cursor_end = 1;
matchparen_deferred_hi_surround_always = true;
matchparen_offscreen = { method = "popup"; };
treesitter = {
enable = true; enable = true;
cursorEnd = true; include_match_words = true;
}; };
matchParen = {
hiSurroundAlways = true;
offscreen = { method = "popup"; };
};
treesitterIntegration = {
enable = true;
includeMatchWords = true;
}; };
}; };
}; };

View File

@@ -8,6 +8,11 @@
loader.systemd-boot.enable = true; loader.systemd-boot.enable = true;
loader.efi.canTouchEfiVariables = true; loader.efi.canTouchEfiVariables = true;
loader.systemd-boot.configurationLimit = 10; loader.systemd-boot.configurationLimit = 10;
loader.grub.gfxmodeEfi = "1024x768";
loader.grub.extraConfig = ''
GRUB_CMDLINE_LINUX_DEFAULT="nomodeset"
GRUB_GFXPAYLOAD_LINUX=1024x768
'';
kernelPackages = pkgs.linuxPackages_latest; kernelPackages = pkgs.linuxPackages_latest;
}; };
}; };

View File

@@ -13,6 +13,8 @@
cliphist cliphist
fail2ban fail2ban
git git
gcc
lldb
hyprland-workspaces hyprland-workspaces
hyprpaper hyprpaper
hyprpicker hyprpicker

View File

@@ -4,6 +4,21 @@
lib.mkEnableOption "enables default system programs"; lib.mkEnableOption "enables default system programs";
}; };
config = lib.mkIf config.movOpts.softwareCfg.sysProgs.enable { config = lib.mkIf config.movOpts.softwareCfg.sysProgs.enable {
environment.etc."shells" = {
enable = true;
text = ''
/run/current-system/sw/bin/zsh
/run/current-system/sw/bin/bash
/run/current-system/sw/bin/zsh
/nix/store/m7l6yzmflrf9hjs8707lk9nkhi6f73n1-zsh-5.9/bin/zsh
/run/current-system/sw/bin/bash
/run/current-system/sw/bin/sh
/nix/store/f33kh08pa7pmy4kvsmsibda46sh46s66-bash-interactive-5.2p37/bin/bash
/nix/store/f33kh08pa7pmy4kvsmsibda46sh46s66-bash-interactive-5.2p37/bin/sh
/bin/sh
/home/pagedmov/Coding/projects/rust/rsh/target/debug/rsh
'';
};
programs = { programs = {
hyprland.enable = lib.mkDefault true; hyprland.enable = lib.mkDefault true;
zsh.enable = lib.mkDefault true; zsh.enable = lib.mkDefault true;

View File

@@ -19,15 +19,16 @@
alsa.enable = true; alsa.enable = true;
alsa.support32Bit = true; alsa.support32Bit = true;
}; };
openssh = {
enable = true;
allowSFTP = true;
};
ratbagd.enable = true;
pcscd.enable = true; pcscd.enable = true;
udev.enable = true; udev.enable = true;
dbus.enable = true; dbus.enable = true;
mullvad-vpn.enable = true; mullvad-vpn.enable = true;
blueman.enable = true; blueman.enable = true;
openssh = {
enable = true;
allowSFTP = true;
};
}; };
}; };
} }

View File

@@ -0,0 +1,14 @@
{ lib, config, pkgs, ... }: {
options = {
movOpts.sysEnv.consoleSettings.enable =
lib.mkEnableOption "enables my console settings";
};
config = lib.mkIf config.movOpts.sysEnv.consoleSettings.enable {
i18n.defaultLocale = "en_US.UTF-8";
console = {
earlySetup = true;
font = "Lat2-Terminus32";
packages = with pkgs; [ terminus_font ];
};
};
}

View File

@@ -1,4 +1,5 @@
{ inputs, nixpkgs, nixvim, config, self, username, host, ... }: { { inputs, nixpkgs, nixvim, config, self, username, host, ... }: {
imports = [ (import ./sddm.nix) ] ++ [ (import ./issue.nix) ] imports = [ (import ./sddm.nix) ] ++ [ (import ./issue.nix) ]
++ [ (import ./nix.nix) ] ++ [ (import ./stylix.nix) ]; ++ [ (import ./nix.nix) ] ++ [ (import ./stylix.nix) ];
#++ [ (import ./console.nix) ];
} }

View File

@@ -4,12 +4,18 @@
lib.mkEnableOption "enables my nixos settings"; lib.mkEnableOption "enables my nixos settings";
}; };
config = lib.mkIf config.movOpts.sysEnv.nixSettings.enable { config = lib.mkIf config.movOpts.sysEnv.nixSettings.enable {
system.stateVersion = "25.05"; system.stateVersion = "24.05";
nix = { nix = {
settings = { settings = {
auto-optimise-store = true;
experimental-features = [ "nix-command" "flakes" ]; experimental-features = [ "nix-command" "flakes" ];
substituters = [ "https://nix-gaming.cachix.org" ]; substituters = [ "https://nix-gaming.cachix.org" ];
}; };
gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 7d";
};
}; };
time.timeZone = "America/New_York"; time.timeZone = "America/New_York";
i18n.defaultLocale = "en_US.UTF-8"; i18n.defaultLocale = "en_US.UTF-8";

View File

@@ -19,9 +19,9 @@ in {
}; };
polarity = "dark"; polarity = "dark";
autoEnable = true; autoEnable = true;
opacity.terminal = 0.5; opacity.terminal = 0.8;
targets = { targets = {
console.enable = true; console.enable = false;
feh.enable = true; feh.enable = true;
grub.enable = true; grub.enable = true;
gtk.enable = true; gtk.enable = true;

View File

@@ -1,4 +1,4 @@
{ host, root, ... }: self: super: { host ? "oganesson", root, ... }: self: super:
let let
extraFigletFonts = super.fetchFromGitHub { extraFigletFonts = super.fetchFromGitHub {
@@ -22,6 +22,7 @@ in
# Packages that I've made # Packages that I've made
tinyfetch = super.callPackage ./tinyfetch/package.nix {}; tinyfetch = super.callPackage ./tinyfetch/package.nix {};
breezex-cursor = super.callPackage ./breezex-cursor/package.nix {}; breezex-cursor = super.callPackage ./breezex-cursor/package.nix {};
rsh = super.callPackage ./rsh/package.nix {};
}; };
myScripts = { myScripts = {
# Scripts written using pkgs.writeShellApplication # Scripts written using pkgs.writeShellApplication

View File

@@ -0,0 +1,19 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.rustPlatform.buildRustPackage {
pname = "rsh";
version = "0.0.1";
src = ./rsh;
cargoHash = "sha256-8Lb7AohSah2A7pcoT2JPDgza0LfIyD897yj4QHNklDw=";
buildInputs = [ ];
meta = {
description = "Modern shell scripting";
homepage = "https://github.com/pagedMov/rsh";
license = pkgs.lib.licenses.gpl3;
maintainers = with pkgs.lib.maintainers; [ pagedMov ];
};
}

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# ^ make editor happy
#
# Use https://direnv.net/ to automatically load the dev shell.
#
if ! has nix_direnv_version || ! nix_direnv_version 3.0.4; then
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.4/direnvrc" "sha256-DzlYZ33mWF/Gs8DDeyjr8mnVmQGx7ASYqA5WlxwvBG4="
fi
watch_file nix/**
watch_file -- **/*.nix
# Adding files to git includes them in a flake
# But it is also a bit much reloading.
# watch_file .git/index .git/HEAD
use flake . --show-trace

26
overlay/pkgs/rsh/rsh/.gitignore vendored Normal file
View File

@@ -0,0 +1,26 @@
target
**/*.rs.bk
.idea
*.iml
/result*
*.log
*~
# cachix tmp file
store-path-pre-build
# Devenv
.devenv*
devenv.local.nix
# direnv
.direnv
# pre-commit
.pre-commit-config.yaml
template/flake.lock
ideas.md
roadmap.md
README.md
file.*

1690
overlay/pkgs/rsh/rsh/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,31 @@
[package]
name = "rsh"
description = "A linux shell written in rust"
publish = false
version = "0.1.0"
edition = "2021"
[profile.release]
debug = true
[dependencies]
regex = "1.11.1"
log = "0.4.22"
tokio = { version = "1.41.1", features = ["full"] }
env_logger = "0.11.5"
thiserror = "2.0.3"
tokio-stream = { version = "0.1.16", features = ["full"] }
lazy_static = "1.5.0"
nix = { version = "0.29.0", features = ["user", "hostname", "fs", "default", "signal", "process", "event", "ioctl"] }
libc = "0.2.167"
once_cell = "1.20.2"
glob = "0.3.1"
bitflags = "2.6.0"
im = "15.1.0"
rustyline = { version = "15.0.0", features = [ "derive" ] }
chrono = "0.4.39"
crossterm = "0.28.1"
skim = "0.15.7"
perf = "0.0.2"
signal-hook = "0.3.17"

View File

@@ -0,0 +1 @@
A shell program written in rust. Currently a very experimental program, though approaching usability. Uses a custom tokenizer/parser with syntax based on bash/sh.

View File

@@ -0,0 +1,5 @@
## Stuff that needs to be done
* rework variable substitution; the logic is too flimsy in it's current state. It currently works with raw strings rather than tokens which makes the logic brittle and largely incompatible with large swathes of the code-base. The hacky implementation of "$@" is a good example of this.
* Implement behavior for shell opt flags

9
overlay/pkgs/rsh/rsh/file.sh Executable file
View File

@@ -0,0 +1,9 @@
(#!python
import sys
word1 = sys.argv[1]
word2 = sys.argv[2]
print(word1,word2)
)

933
overlay/pkgs/rsh/rsh/flake.lock generated Normal file
View File

@@ -0,0 +1,933 @@
{
"nodes": {
"cachix": {
"inputs": {
"devenv": [
"crate2nix"
],
"flake-compat": [
"crate2nix"
],
"nixpkgs": "nixpkgs",
"pre-commit-hooks": [
"crate2nix"
]
},
"locked": {
"lastModified": 1709700175,
"narHash": "sha256-A0/6ZjLmT9qdYzKHmevnEIC7G+GiZ4UCr8v0poRPzds=",
"owner": "cachix",
"repo": "cachix",
"rev": "be97b37989f11b724197b5f4c7ffd78f12c8c4bf",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "latest",
"repo": "cachix",
"type": "github"
}
},
"cachix_2": {
"inputs": {
"devenv": [
"crate2nix",
"crate2nix_stable"
],
"flake-compat": [
"crate2nix",
"crate2nix_stable"
],
"nixpkgs": "nixpkgs_2",
"pre-commit-hooks": [
"crate2nix",
"crate2nix_stable"
]
},
"locked": {
"lastModified": 1716549461,
"narHash": "sha256-lHy5kgx6J8uD+16SO47dPrbob98sh+W1tf4ceSqPVK4=",
"owner": "cachix",
"repo": "cachix",
"rev": "e2bb269fb8c0828d5d4d2d7b8d09ea85abcacbd4",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "latest",
"repo": "cachix",
"type": "github"
}
},
"cachix_3": {
"inputs": {
"devenv": [
"crate2nix",
"crate2nix_stable",
"crate2nix_stable"
],
"flake-compat": [
"crate2nix",
"crate2nix_stable",
"crate2nix_stable"
],
"nixpkgs": "nixpkgs_3",
"pre-commit-hooks": [
"crate2nix",
"crate2nix_stable",
"crate2nix_stable"
]
},
"locked": {
"lastModified": 1716549461,
"narHash": "sha256-lHy5kgx6J8uD+16SO47dPrbob98sh+W1tf4ceSqPVK4=",
"owner": "cachix",
"repo": "cachix",
"rev": "e2bb269fb8c0828d5d4d2d7b8d09ea85abcacbd4",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "latest",
"repo": "cachix",
"type": "github"
}
},
"crate2nix": {
"inputs": {
"cachix": "cachix",
"crate2nix_stable": "crate2nix_stable",
"devshell": "devshell_3",
"flake-compat": "flake-compat_3",
"flake-parts": "flake-parts_3",
"nix-test-runner": "nix-test-runner_3",
"nixpkgs": "nixpkgs_6",
"pre-commit-hooks": "pre-commit-hooks_3"
},
"locked": {
"lastModified": 1734429562,
"narHash": "sha256-V2XNs3Ir8WXNHdocfzkR/fu0FzkZ9uTDJkVecxJrGmQ=",
"owner": "nix-community",
"repo": "crate2nix",
"rev": "8537c2d7cb623679aaeff62c4c4c43a91566ab09",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "crate2nix",
"type": "github"
}
},
"crate2nix_stable": {
"inputs": {
"cachix": "cachix_2",
"crate2nix_stable": "crate2nix_stable_2",
"devshell": "devshell_2",
"flake-compat": "flake-compat_2",
"flake-parts": "flake-parts_2",
"nix-test-runner": "nix-test-runner_2",
"nixpkgs": "nixpkgs_5",
"pre-commit-hooks": "pre-commit-hooks_2"
},
"locked": {
"lastModified": 1719760004,
"narHash": "sha256-esWhRnt7FhiYq0CcIxw9pvH+ybOQmWBfHYMtleaMhBE=",
"owner": "nix-community",
"repo": "crate2nix",
"rev": "1dee214bb20855fa3e1e7bb98d28922ddaff8c57",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "0.14.1",
"repo": "crate2nix",
"type": "github"
}
},
"crate2nix_stable_2": {
"inputs": {
"cachix": "cachix_3",
"crate2nix_stable": "crate2nix_stable_3",
"devshell": "devshell",
"flake-compat": "flake-compat",
"flake-parts": "flake-parts",
"nix-test-runner": "nix-test-runner",
"nixpkgs": "nixpkgs_4",
"pre-commit-hooks": "pre-commit-hooks"
},
"locked": {
"lastModified": 1712821484,
"narHash": "sha256-rGT3CW64cJS9nlnWPFWSc1iEa3dNZecVVuPVGzcsHe8=",
"owner": "nix-community",
"repo": "crate2nix",
"rev": "42883afcad3823fa5811e967fb7bff54bc3c9d6d",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "0.14.0",
"repo": "crate2nix",
"type": "github"
}
},
"crate2nix_stable_3": {
"inputs": {
"flake-utils": "flake-utils"
},
"locked": {
"lastModified": 1702842982,
"narHash": "sha256-A9AowkHIjsy1a4LuiPiVP88FMxyCWK41flZEZOUuwQM=",
"owner": "nix-community",
"repo": "crate2nix",
"rev": "75ac2973affa6b9b4f661a7b592cba6e4f51d426",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "0.12.0",
"repo": "crate2nix",
"type": "github"
}
},
"devshell": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": [
"crate2nix",
"crate2nix_stable",
"crate2nix_stable",
"nixpkgs"
]
},
"locked": {
"lastModified": 1717408969,
"narHash": "sha256-Q0OEFqe35fZbbRPPRdrjTUUChKVhhWXz3T9ZSKmaoVY=",
"owner": "numtide",
"repo": "devshell",
"rev": "1ebbe68d57457c8cae98145410b164b5477761f4",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"devshell_2": {
"inputs": {
"flake-utils": "flake-utils_3",
"nixpkgs": [
"crate2nix",
"crate2nix_stable",
"nixpkgs"
]
},
"locked": {
"lastModified": 1717408969,
"narHash": "sha256-Q0OEFqe35fZbbRPPRdrjTUUChKVhhWXz3T9ZSKmaoVY=",
"owner": "numtide",
"repo": "devshell",
"rev": "1ebbe68d57457c8cae98145410b164b5477761f4",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"devshell_3": {
"inputs": {
"flake-utils": "flake-utils_4",
"nixpkgs": [
"crate2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1711099426,
"narHash": "sha256-HzpgM/wc3aqpnHJJ2oDqPBkNsqWbW0WfWUO8lKu8nGk=",
"owner": "numtide",
"repo": "devshell",
"rev": "2d45b54ca4a183f2fdcf4b19c895b64fbf620ee8",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"flake-compat": {
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"revCount": 57,
"type": "tarball",
"url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.0.1/018afb31-abd1-7bff-a5e4-cff7e18efb7a/source.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"
}
},
"flake-compat_2": {
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"revCount": 57,
"type": "tarball",
"url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.0.1/018afb31-abd1-7bff-a5e4-cff7e18efb7a/source.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"
}
},
"flake-compat_3": {
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"revCount": 57,
"type": "tarball",
"url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.0.1/018afb31-abd1-7bff-a5e4-cff7e18efb7a/source.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"crate2nix",
"crate2nix_stable",
"crate2nix_stable",
"nixpkgs"
]
},
"locked": {
"lastModified": 1719745305,
"narHash": "sha256-xwgjVUpqSviudEkpQnioeez1Uo2wzrsMaJKJClh+Bls=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "c3c5ecc05edc7dafba779c6c1a61cd08ac6583e9",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_2": {
"inputs": {
"nixpkgs-lib": [
"crate2nix",
"crate2nix_stable",
"nixpkgs"
]
},
"locked": {
"lastModified": 1719745305,
"narHash": "sha256-xwgjVUpqSviudEkpQnioeez1Uo2wzrsMaJKJClh+Bls=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "c3c5ecc05edc7dafba779c6c1a61cd08ac6583e9",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_3": {
"inputs": {
"nixpkgs-lib": [
"crate2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1712014858,
"narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "9126214d0a59633752a136528f5f3b9aa8565b7d",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_3": {
"inputs": {
"systems": "systems_3"
},
"locked": {
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_4": {
"inputs": {
"systems": "systems_4"
},
"locked": {
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_5": {
"inputs": {
"systems": "systems_5"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_6": {
"inputs": {
"systems": "systems_6"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"crate2nix",
"crate2nix_stable",
"crate2nix_stable",
"pre-commit-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"gitignore_2": {
"inputs": {
"nixpkgs": [
"crate2nix",
"crate2nix_stable",
"pre-commit-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"gitignore_3": {
"inputs": {
"nixpkgs": [
"crate2nix",
"pre-commit-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"nix-test-runner": {
"flake": false,
"locked": {
"lastModified": 1588761593,
"narHash": "sha256-FKJykltAN/g3eIceJl4SfDnnyuH2jHImhMrXS2KvGIs=",
"owner": "stoeffel",
"repo": "nix-test-runner",
"rev": "c45d45b11ecef3eb9d834c3b6304c05c49b06ca2",
"type": "github"
},
"original": {
"owner": "stoeffel",
"repo": "nix-test-runner",
"type": "github"
}
},
"nix-test-runner_2": {
"flake": false,
"locked": {
"lastModified": 1588761593,
"narHash": "sha256-FKJykltAN/g3eIceJl4SfDnnyuH2jHImhMrXS2KvGIs=",
"owner": "stoeffel",
"repo": "nix-test-runner",
"rev": "c45d45b11ecef3eb9d834c3b6304c05c49b06ca2",
"type": "github"
},
"original": {
"owner": "stoeffel",
"repo": "nix-test-runner",
"type": "github"
}
},
"nix-test-runner_3": {
"flake": false,
"locked": {
"lastModified": 1588761593,
"narHash": "sha256-FKJykltAN/g3eIceJl4SfDnnyuH2jHImhMrXS2KvGIs=",
"owner": "stoeffel",
"repo": "nix-test-runner",
"rev": "c45d45b11ecef3eb9d834c3b6304c05c49b06ca2",
"type": "github"
},
"original": {
"owner": "stoeffel",
"repo": "nix-test-runner",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1700612854,
"narHash": "sha256-yrQ8osMD+vDLGFX7pcwsY/Qr5PUd6OmDMYJZzZi0+zc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "19cbff58383a4ae384dea4d1d0c823d72b49d614",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1715534503,
"narHash": "sha256-5ZSVkFadZbFP1THataCaSf0JH2cAH3S29hU9rrxTEqk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2057814051972fa1453ddfb0d98badbea9b83c06",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1715534503,
"narHash": "sha256-5ZSVkFadZbFP1THataCaSf0JH2cAH3S29hU9rrxTEqk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2057814051972fa1453ddfb0d98badbea9b83c06",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_4": {
"locked": {
"lastModified": 1719506693,
"narHash": "sha256-C8e9S7RzshSdHB7L+v9I51af1gDM5unhJ2xO1ywxNH8=",
"path": "/nix/store/4p0avw1s3vf27hspgqsrqs37gxk4i83i-source",
"rev": "b2852eb9365c6de48ffb0dc2c9562591f652242a",
"type": "path"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_5": {
"locked": {
"lastModified": 1719506693,
"narHash": "sha256-C8e9S7RzshSdHB7L+v9I51af1gDM5unhJ2xO1ywxNH8=",
"path": "/nix/store/4p0avw1s3vf27hspgqsrqs37gxk4i83i-source",
"rev": "b2852eb9365c6de48ffb0dc2c9562591f652242a",
"type": "path"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_6": {
"locked": {
"lastModified": 1712026416,
"narHash": "sha256-N/3VR/9e1NlN49p7kCiATiEY6Tzdo+CbrAG8kqCQKcI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "080a4a27f206d07724b88da096e27ef63401a504",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_7": {
"locked": {
"lastModified": 1735291276,
"narHash": "sha256-NYVcA06+blsLG6wpAbSPTCyLvxD/92Hy4vlY9WxFI1M=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "634fd46801442d760e09493a794c4f15db2d0cbb",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_8": {
"locked": {
"lastModified": 1728538411,
"narHash": "sha256-f0SBJz1eZ2yOuKUr5CA9BHULGXVSn6miBuUWdTyhUhU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b69de56fac8c2b6f8fd27f2eca01dcda8e0a4221",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"pre-commit-hooks": {
"inputs": {
"flake-compat": [
"crate2nix",
"crate2nix_stable",
"crate2nix_stable",
"flake-compat"
],
"gitignore": "gitignore",
"nixpkgs": [
"crate2nix",
"crate2nix_stable",
"crate2nix_stable",
"nixpkgs"
],
"nixpkgs-stable": [
"crate2nix",
"crate2nix_stable",
"crate2nix_stable",
"nixpkgs"
]
},
"locked": {
"lastModified": 1719259945,
"narHash": "sha256-F1h+XIsGKT9TkGO3omxDLEb/9jOOsI6NnzsXFsZhry4=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "0ff4381bbb8f7a52ca4a851660fc7a437a4c6e07",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"type": "github"
}
},
"pre-commit-hooks_2": {
"inputs": {
"flake-compat": [
"crate2nix",
"crate2nix_stable",
"flake-compat"
],
"gitignore": "gitignore_2",
"nixpkgs": [
"crate2nix",
"crate2nix_stable",
"nixpkgs"
],
"nixpkgs-stable": [
"crate2nix",
"crate2nix_stable",
"nixpkgs"
]
},
"locked": {
"lastModified": 1719259945,
"narHash": "sha256-F1h+XIsGKT9TkGO3omxDLEb/9jOOsI6NnzsXFsZhry4=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "0ff4381bbb8f7a52ca4a851660fc7a437a4c6e07",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"type": "github"
}
},
"pre-commit-hooks_3": {
"inputs": {
"flake-compat": [
"crate2nix",
"flake-compat"
],
"flake-utils": "flake-utils_5",
"gitignore": "gitignore_3",
"nixpkgs": [
"crate2nix",
"nixpkgs"
],
"nixpkgs-stable": [
"crate2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1712055707,
"narHash": "sha256-4XLvuSIDZJGS17xEwSrNuJLL7UjDYKGJSbK1WWX2AK8=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "e35aed5fda3cc79f88ed7f1795021e559582093a",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"type": "github"
}
},
"root": {
"inputs": {
"crate2nix": "crate2nix",
"flake-utils": "flake-utils_6",
"nixpkgs": "nixpkgs_7",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": "nixpkgs_8"
},
"locked": {
"lastModified": 1735352767,
"narHash": "sha256-3zXufMRWUdwmp8/BTmxVW/k4MyqsPjLnnt/IlQyZvhc=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "a16b9a7cac7f4d39a84234d62e91890370c57d76",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_3": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_4": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_5": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_6": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -0,0 +1,47 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
rust-overlay.url = "github:oxalica/rust-overlay";
crate2nix.url = "github:nix-community/crate2nix";
};
outputs = { self, crate2nix, nixpkgs, flake-utils, rust-overlay }:
flake-utils.lib.eachDefaultSystem
(system:
let
overlays = [ (import rust-overlay) ];
cargoNix = crate2nix.tools.${system}.appliedCargoNix {
name = "rsh";
src = ./.;
};
pkgs = import nixpkgs {
inherit system overlays;
};
in
with pkgs;
rec {
checks = {
rustnix = cargoNix.rootCrate.build.override {
runTests = true;
};
};
packages = {
rustnix = cargoNix.rootCrate.build;
default = packages.rustnix;
inherit rust-toolchain;
};
devShells.default = mkShell {
buildInputs = [
rust-bin.nightly.latest.default
clang
llvm
libclang
pam
];
shellHook = ''
exec zsh
'';
};
}
);
}

View File

@@ -0,0 +1,159 @@
Bash limitations:
1. Data structures
- Arrays are extremely clunky to make use of and have extremely specific syntax symantics in regards to indexing for instance
- Essentially no native support for dictionaries
- JSON parsing and manipulation requires tools like jq which themselves have their own syntax rules to learn. Requiring these external tools could be eliminated entirely by simply having support for JSON-like structures
2. Error handling
- Error handling in bash is another pain point for the language.
- Propagating errors is entirely manual and modern shells mainly use exit codes to communicate errors, which for codes beyond 0 and 1 are essentially meaningless to all but the most dedicated shell users.
- Essentially no tracebacks, and no good way to pinpoint where and when an error occurs.
- This could be fixed by taking queues from python or rust with their very robust error handling paradigms
- Solving this problem alone would vastly improve developer experience with shell scripting
3. String operation
- String operation in bash often requires the use of external tools such as awk, sed, or cut which each have their own syntax rules that you need to memorize. I mean, awk is literally it's own programming language.
- No multiline strings except with heredocs, which are an extremely clunky and incompatible method of doing so as heredocs can only be used with input redirection to something else.
- rush could include builtins similar to most modern languages like split, format, etc
4. Syntax
- Syntax in bash is often extremely mind-numbing to read. Even string operations of a moderate complexity (e.g. a cat into a sed into an awk) will require you to remember how to read syntax semantics and remember what individual flags do for several different commands as you try to mentally parse the pipeline.
- This can easily be cleaned up by having builtins with consistent syntax for the most common operations that are usually outsourced to external commands.
5. Types
- Everything in bash is a string, there are no types. This often makes script behavior difficult to predict.
- Since everything is a string, it's difficult for your script to know if it's working with the right kind of data without overly verbose conditional checks.
- A typing system will make scripts far easier to reason about, far easier to maintain, and far easier to debug
6. Builtins
- If you've ever used bash on a very minimal system, you'll find that it is woefully limited with just the core gnu utils installed.
- Much of the functionality of modern bash comes from external binaries like grep, awk, and sed. This raises issues when running scripts on systems that may not have these binaries installed.
- Many of the functionalities most commonly afforded by external commands could easily be adapted into a standard library of sorts.
7. Prone to errors
- Bash syntax is riddled with pitfalls and gotchas
- Quoting rules, working with elements in arrays, passing arguments to functions, the list goes on
- Making the quoting and escaping rules clearer and also allowing functions to be defined like 'function(arg1,arg2) {}' would make it leagues easier to work with.
8. Concurrency and Async
- While bash allows you to run processes in the background non-blockingly, the support for this functionality is woefully minimal.
- There is essentially no native support for async programming beyond forking processes.
9. Error messages
- Bash can be extremely frustrating to work with at times. Error messages are often extremely unhelpful, usually just canned responses rather than anything specific to the code being run.
- A single unclosed delimiter in a long script becomes a hunt for a needle in a haystack.
- This is actually a pretty simple fix too
10. Portability issues
- It is very often that bash scripts will work on one machine and not on another. This is because shells in general are very dependent upon state, which is obviously not shared between machines. The dependency on external tools and environment state leads to unpredictability across machines and platforms.
- A decent approach for this might be to check for the availability of each command specified in a script and then produce an error message listing the missing commands, rather than running it and having the script actually execute code until it reaches something that it can't do, leading to unpredictable, half-finished logic
11. No real way to test code
- The best you can really do is create a separate script that sources another script and then create test functions in the second file. This is of course a very clunky way to test code and sourcing the other file can have unintended side effects.
# Main Feature Ideas
1. Detailed error messages, fine grained error handling
- In order to bring shell scripting to the modern age, error messages and error handling must be on par with that of Rust or Python.
- Line/Column numbers, "try except" blocks, the whole 9 yards
2. Multiple shebangs in one script
- Shell scripting has limitations, just like every other language. When you run into something that isn't well suited to shell scripting, your only option is to either suck it up and do it in bash anyway, or create an entire separate file to solve that one problem and then execute it in your script
- This could be solved by allowing users to write shebangs in subshells, allowing the script to dynamically switch interpreters to execute code in other languages in said subshells. This way you could get the granular input/output control of shell scripting alongside the power of higher order languages like python.
3. Macros
- Think macros in Rust, but brought to shell scripting. The syntax would be something like this: `split!echo "hello world" -> ["hello","world"]` or `replace("world","rush")!echo "hello world" -> "hello rush"`.
- This would allow for users to dynamically create operations that can be applied to any command that produces the expected output
4. Types
- Shell scripting to this day still has no real support for types. This feature alone would be groundbreaking for shell scripting.
- Adding not only proper native support for booleans and floats, but also higher order data structures such as dictionaries or sets would revolutionize what shell scripting is capable of.
5. Standard Library
- The GNU core utils alone provide woefully incomplete utility. Bash often relies on external commands like `sed`, `awk`, or `grep` in order to perform many of it's main features. This produces a lack of portability as scripts have to make many assumptions about the end user's environment.
- This could be fixed by simply including many of the most common operations performed in bash (pattern searching, regex operations, field extraction) into macros that are built into rush. This would not only increase portability, it would also unify syntax, reducing the mental overhead associated with needing to learn many different bespoke flags and syntaxes for various commands, especially in the case of `awk` and `sed`.
- This is probably the most ambitious part of this project, so I should most like try to get the above ideas implemented before I even begin to approach this.
# Opinionation of Features
* Type declarations
- I want rush to be both backwards compatible with bash while also giving greater utility as a scripting language.
- This means that variable declarations such as `var=value` should remain valid. Bash is weakly typed by default, so the normal variable declaration syntax should work that way.
- However, I also wish to include new shell builtins that allow for strongly typing variables. Stuff like `int var=10` or `string var="hello"` so that the benefits of strong typing (robustness, predictability) can be reaped for longer/more complex scripts.
- For arrays, I want to keep them as they are and also maintain the usual gross syntax for working with them, just for backwards compatibility as a legacy feature. Eventually I want to implement a better type for this similar to Python's lists.
* Macros
- I want to implement macros in a way that makes sense in the context of shell scripting. I'll include several macros that are in sort of a standard library, but each macro will essentially be a command expansion. This is how I want it to work: `split @ echo "hello world" = ["hello", "world"]` would internally be the same as writing `echo "hello world" | tr ' ' '\n'`. Macros would be defined using a syntax like `macro split { "$cmd" | tr ' ' '\n' }` and there would even be support for macros with arguments.
- My idea for handling arguments is to do keep parameters passed before the '@' as positional params for the macro, and then pass input from following commands into a variable '$~'.
- The argument for macros as a feature is to provide a middleground between aliases and functions. There are many cases where aliases are too limited and functions are somewhat overkill, like in the case of the above split macro.
- The main use case will mainly be code *injection* rather than code *execution*. This allows for dynamic generation of code, such as generating many function definitions. This can heavily reduce boilerplate and make scripts more expressive.
- Macros provide inline substitution while functions produce a brand new scope in a subshell. Macros are more suited to logic that can be easily written on one line, and are therefore too simple to really justify an entire function definition, but are slightly too complex to be put into an alias, while still being an operation that is performed frequently.
* Builtins
- Many core functionalities of shell scripting are actually offloaded to external commands like awk, grep, and sed. While these tools are *extremely* powerful, they are mostly used for extremely simple tasks. I can probably count on one hand the amount of times I've had to make real use of awk's full capabilities rather than just using it for field extraction, for instance. These *simple* functionalities can easily be implemented as builtins, and users could utilize the corresponding external commands if they require their more advanced functionalities.
- Including these as builtins opens the door for having a library of builtin macros such as split!, find("pattern")!, or extract(' ',[1,3])! for instance.
* Many Interpreters
- Shell scripting is powerful, but not perfect. There are many cases where you will run into a problem in a script that shell scripting struggles with but Python handles effortlessly, for instance. The builtins I wish to include will most likely not cover these bases.
- Therefore, I wish to expand subshell syntax to allow users to declare a shebang in subshells to dynamically offload subshell content to a secondary interpreter. Here's an example:
```
#!/usr/bin/env rush
echo "Greetings from rush!"
(
#!/usr/bin/env python3
print("Greetings from python!")
)
```
- To achieve a similar functionality in bash, you have to use heredocs which are a clunky and archaic feature that is incompatible with modern development for many reasons, most of which being that heredocs exist entirely as a string until runtime, meaning that IDEs and language servers are completely unable to reason about them.
- A language server for rush would be able to dynamically update syntax highlighting in subshells to match the chosen interpreter, ideally.
- This centralization of concerns would allow users to utilize the system-level power of the shell and programmatic power of modern scripting languages all in the same place with a simple and intuitive syntax
- *Note: Have to figure out how to move variables from the outer shell to the inner one. Probably just expanding them during the tokenization I guess.*
* Shebangs
- Shebangs can be expanded upon to make handling arguments much clearer and self-documenting. For instance: `#!/usr/bin/env rush arg1 arg2=default arg3`, and then reference the args in the script like `$arg1`, `$arg2`, `$arg3`, calling the script like `./script arg1 arg2 arg3`. This is much easier to reason about than the usual `$1`, `$2`, etc. syntax and heavily reduces the boilerplate usually included in scripts used to improve readability
- Rush could additionally validate the arguments at runtime, raising an error if any are missing.
- If no args are specified in the shebang, Rush would default to using traditional positional variable syntax.
- This could even be extended to include the strong typing system mentioned earlier: `#!/usr/bin/env rush int:arg1 string:arg2="default" list:arg3`
- Scripts that require certain arguments but accept further arguments could be written like: `#!/usr/bin/env rush arg1 arg2 ...` and then the extra positional args could be accessed using the original "$@" syntax or something like "$args"
## Macro ideas
- `split !` -> splits a string, optionally takes an arg as a delimiter like `split "," ! echo "foo,bar,baz"`
- `replace <word1> <word2> !` -> simple find and replace in a string or file
- `range <int1> <int2> !` -> needs work, but this idea is useful
- `filter <pattern> !` -> only print lines from cmd output with <pattern>
- `toupper/tolower !` -> change all text to upper or lower case
- `wordcount/linecount/charcount !` -> self explanatory
- `sum/max/average/othermathstuff !` -> simple math operations
- `waitall !` -> wait for all background operations to finish
### newer version
- called like `macro <args> @ <input_source>?` because the exclamation point looks too much like a pipe
- Insert code to be executed once it is reached
- Can be used to generate function definitions dynamically
- Or reduce boilerplate for error handling, etc
## Types to include
from python:
- ints
- floats
- booleans
- lists
- dictionaries
new types:
- file: stores a file in a variable. will have builtins that allow for more fine grained control than simply storing the path as a string in a variable
- bytes: raw data, essentially an abstraction over a Vec<u8> in rust.
## Misc feature ideas
- Some way to tell if you terminal is in an ssh session or a nested shell? Alternate prompt or color scheme?
- Actually having color scheme support in the first place
- Fuzzy finder autocompletion window
- Fuzzy finder integrated as builtin?
- A way to run scripts/programs in a totally clean environment, similar to nix package building
- Terminal display layers (sounds painful)

View File

@@ -0,0 +1,25 @@
{ inputs, lib, ... }: {
imports = [
inputs.devshell.flakeModule
];
config.perSystem =
{ pkgs
, ...
}: {
config.devshells.default = {
imports = [
"${inputs.devshell}/extra/language/c.nix"
# "${inputs.devshell}/extra/language/rust.nix"
];
commands = with pkgs; [
{ package = rust-toolchain; category = "rust"; }
];
language.c = {
libraries = lib.optional pkgs.stdenv.isDarwin pkgs.libiconv;
};
};
};
}

View File

@@ -0,0 +1,18 @@
{ inputs, ... }:
let
overlays = [
(import inputs.rust-overlay)
(self: super: assert !(super ? rust-toolchain); rec {
rust-toolchain = super.rust-bin.fromRustupToolchainFile ../../rust-toolchain.toml;
# buildRustCrate/crate2nix depend on this.
rustc = rust-toolchain;
cargo = rust-toolchain;
})
];
in
{
perSystem = { system, ... }: {
_module.args.pkgs = import inputs.nixpkgs { inherit system overlays; config = { }; };
};
}

View File

@@ -0,0 +1,202 @@
## TODO:
### priority
- [ ] **Phase 1** - implement essential bash compatibility
- [ ] **Phase 2** - implement essential rsh features
- [ ] **Phase 3** - implement QoL performance and UX refinements (testing, async, syntax highlighting)
### rsh features
- [ ] macros and custom builtins
- [ ] macro definition syntax
- [ ] macro execution syntax
- [ ] macros with args
- [ ] builtin macros for very common tasks
- [ ] string replacement
- [ ] pattern searching
- [ ] field extraction
- [ ] handling for JSON and modern data structures
- [ ] modern error handling such as stack tracing and try/except (logging macros as well?)
- [ ] includes line/col numbers
- [ ] shows the line where the error occured like python/rust
- [ ] points to the exact location of the error like python/rust
- [ ] typing system - typing builtins like int, string, float, etc along with the usual weak bash types
- [ ] support for default bash typing
- [ ] support for strongly typing function arguments like function(int:arg1,string:arg2)
- [ ] improved bash control structures?
- [ ] support for explicitly declaring expected arguments
- [ ] do this in the shebang `#!/usr/bin/env rsh arg1 arg2`
- [ ] do this in functions `function(arg1,arg2)`
- [ ] allow for extra arguments `#!/usr/bin/env rsh arg1 arg2 ...`, `function(arg1, arg2, ...)`
- [ ] allow for default values `arg1=default`, `string:arg1="hello"`
- [ ] syntax highlighting in shell prompt
- [ ] language server?
- [ ] better support for asynchronous scripting
- [ ] portability features
- [ ] detection of missing commands at script runtime
- [ ] detection of unset environment variables at script runtime
- [ ] builtin testing framework for scripts using asserts
### bash features
- [x] unquoted string suppport. (space character taken care of) (that one may still be a bit buggy)
- [x] "quoted" string support. (space character taken care of)
- [x] 'quoted' string support. (space character taken care of)
- [ ] \`backticked\` string support. (space character taken care of)
- [ ] binary numbers support (uint only for now ?). 0b mark. (i64 by default)
- [ ] octal numbers support (uint only for now ?). 0 mark. (i64 by default)
- [ ] hexadecimal numbers support (uint only for now ?). 0x mark. (i64 by default)
- [ ] (signed) int support (i64 by default).
- [ ] (signed) float support (f64 by default). exponent correctly parsed.
- [x] variable assignment. (type is autodetected)
- [ ] array assignment. (type is autodetected)
- [ ] shebang (#!) and comments are correctly parsed.
- [ ] : syntax
- [ ] compgen builtin.
- [ ] complete builtin.
- [ ] compopt builtin.
- [ ] fc builtin.
- [ ] history builtin.
- [ ] local builtin.
- [ ] mapfile builtin.
- [ ] readarray builtin.
- [ ] return builtin.
- [ ] shift builtin.
- [ ] test builtin.
- [ ] trap builtin.
- [ ] ulimit builtin.
- [ ] umask builtin.
- [ ] unalias builtin.
- [ ] arrays support (single dimension, can be both associative and indexed, and store int, float or string.
- [x] $variable, ${variable} and parameter ($1 etc).
- [ ] ${parameter-default} ${parameter:-default} If parameter not set, use default.
- [ ] ${parameter=default}, ${parameter:=default} If parameter not set, set it to default.
- [ ] ${parameter+alt_value}, ${parameter:+alt_value} If parameter set, use alt_value, else use null string.
- [ ] ${parameter?err_msg}, ${parameter:?err_msg} If parameter set, use it, else print err_msg and abort the script with an exit status of 1.
- [ ] variable builtin ${#string} (var length).
- [ ] variable builtin ${string:pos} (extract substring at pos).
- [ ] variable builtin ${string:pos:len} (extract len substring at pos).
- [ ] variable builtin ${string#substr} (deletes shortest match of $substr from front of $string).
- [ ] variable builtin ${string##substr} (deletes longest match of $substr from front of $string).
- [ ] variable builtin ${string%substr} (deletes shortest match of $substr from back of $string).
- [ ] variable builtin ${string%%substr} (deletes longest match of $substr from back of $string).
- [ ] variable builtin ${string/substr/repl} (Replace first match of $substr with $repl).
- [ ] variable builtin ${string//substr/repl} (Replace all matches of $substr with $repl).
- [ ] variable builtin ${string/#substr/repl} (If $substr matches front end of $string, substitute $repl for $substr.).
- [ ] variable builtin ${string/%substr/repl} (If $substr matches back end of $string, substitute $repl for $substr.).
- [ ] variable builtin ${!varprefix*}, ${!varprefix@} (Matches names of all previously declared variables beginning with varprefix.).
- [ ] alias substitution and builtin.
- [ ] $(command) substitution (kind of similar to backtick).
- [ ] ~ expansion.
- [ ] !! expansion (history).
- [ ] {} expansion.
- [ ] $(( )) arithmetic expansion.
- [ ] [[ ]] expansion.
- [ ] [ ] expansion.
- [ ] * ? etc expansion.
- [ ] regexp support =~
- [ ] POSIX characters classes [:alnum:] matches alphabetic or numeric characters. This is equivalent to A-Za-z0-9.
- [ ] POSIX characters classes [:alpha:] matches alphabetic characters. This is equivalent to A-Za-z.
- [ ] POSIX characters classes [:blank:] matches a space or a tab.
- [ ] POSIX characters classes [:cntrl:] matches control characters.
- [ ] POSIX characters classes [:digit:] matches (decimal) digits. This is equivalent to 0-9.
- [ ] POSIX characters classes [:graph:] (graphic printable characters). Matches characters in the range of ASCII 33 - 126. This is the same as [:print:], below, but excluding the space character.
- [ ] POSIX characters classes [:lower:] matches lowercase alphabetic characters. This is equivalent to a-z.
- [ ] POSIX characters classes [:print:] (printable characters). Matches characters in the range of ASCII 32 - 126. This is the same as [:graph:], above, but adding the space character.
- [ ] POSIX characters classes [:space:] matches whitespace characters (space and horizontal tab).
- [ ] POSIX characters classes [:upper:] matches uppercase alphabetic characters. This is equivalent to A-Z.
- [ ] POSIX characters classes [:xdigit:] matches hexadecimal digits. This is equivalent to 0-9A-Fa-f.
- [x] if elif else fi.
- [ ] case "$var" in "value") command ;; "value2") command ;; esac
- [ ] for n in list do done { } may be used instead of do done
- [ ] for ((a=1; a<bla; a++)) do done { } may be used instead of do done
- [ ] while [condition] do done (optional brackets)
- [ ] while (( condition )) do done
- [ ] until [condition] do done
- [ ] do done
- [ ] break
- [ ] continue
- [ ] function function_name() { } and function() { }
- [ ] select variable in list (optional in list) do command break done
- [ ] command execution
- [x] pipes
- [x] > < >> << 2>&1 etc redirections. dont forget <<EOF kind.
- [ ] <(command list) >(command list) process substitution.
- [x] || && operators
- [ ] echo (complete support)
- [ ] printf
- [ ] read
- [ ] cd
- [ ] pwd
- [ ] popd
- [ ] pushd
- [ ] dirs
- [ ] let += -= /= \*= %=
- [ ] eval
- [ ] set
- [ ] unset
- [ ] export
- [ ] declare
- [ ] typeset
- [ ] readonly
- [ ] getopts
- [ ] source .
- [ ] exit
- [ ] exec
- [ ] shopt
- [ ] caller
- [ ] true
- [ ] false
- [ ] type
- [ ] hash
- [ ] bind
- [ ] help
- [ ] jobs
- [ ] disown
- [ ] bg
- [ ] fg
- [ ] wait
- [ ] suspend
- [ ] logout
- [ ] times
- [ ] kill
- [ ] killall
- [ ] command
- [ ] builtin
- [ ] enable
- [ ] autoload
- [ ] .pest files will need a complete overhaul to be cleaner and better structured.
### core
- [x] Variables management (simple variables, aliases and single dimension arrays).
- [x] Variables assignment.
- [ ] Arrays assignment.
- [x] $variable, ${variable} and parameter ($1 etc) expansion.
- [ ] ${parameter-default} ${parameter:-default} expansion.
- [ ] ${parameter=default}, ${parameter:=default} expansion.
- [ ] ${parameter+alt_value}, ${parameter:+alt_value} expansion.
- [ ] ${parameter?err_msg}, ${parameter:?err_msg} expansion.
- [ ] variable builtin ${#string} expansion.
- [ ] variable builtin ${string:pos} expansion.
- [ ] variable builtin ${string:pos:len} expansion.
- [ ] variable builtin ${string#substr} expansion.
- [ ] variable builtin ${string##substr} expansion.
- [ ] variable builtin ${string%substr} expansion.
- [ ] variable builtin ${string%%substr} expansion.
- [ ] variable builtin ${string/substr/repl} expansion.
- [ ] variable builtin ${string//substr/repl} expansion.
- [ ] variable builtin ${string/#substr/repl} expansion.
- [ ] variable builtin ${string/%substr/repl} expansion.
- [ ] variable builtin ${!varprefix*}, ${!varprefix@} expansion.
- [ ] Write everything linked to builtins, pipes etc (yeah, that will be *very* long).
- [ ] Have a 100% code coverage when it comes to documentation *and* testing.
- [ ] Multi-lingual support (i18n ? l20n.rs ?).
- [ ] Proper color management (using [term](https://crates.io/crates/term) crate maybe ?).
- [ ] Think of ways to get speed with rsh (read: be faster than Bash). JIT ? Some kind of « parsed script ready to be executed » ?
- [ ] Support float processing. (kind of done, see variable management above).
- [ ] Deprecate several bashy thing (older versions compatibility etc, bash is so much of a noodles plate now due to history that I wont be able to cover every point so Ill have to focus).
- [ ] Use [hashbrown](https://github.com/Amanieu/hashbrown) instead of [seahash](https://crates.io/crates/seahash). It will be used by [default](https://github.com/rust-lang/rust/pull/58623) in std once rust 1.36 is out.
- [x] Use of cargo clippy.
- [x] Use of cargo fmt.
- [ ] Use of tarpaulin.
- [ ] Use of criterion for benchmarking.
- [ ] Put tests and benches in their own directories.
- [ ] Use of error-chain, some fuzzer ?
- [ ] So many other things. multidimensionnal arrays ?

View File

@@ -0,0 +1,46 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "proc-macro2"
version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rsh_derive"
version = "0.1.0"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "syn"
version = "2.0.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"

View File

@@ -0,0 +1,11 @@
[package]
name = "rsh_derive"
version = "0.1.0"
edition = "2024"
[lib]
proc-macro = true
[dependencies]
quote = "1.0.37"
syn = "2.0.91"

View File

@@ -0,0 +1,53 @@
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Node)]
pub fn derive_node(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let expanded = quote! {
impl Node for #name {
fn span(&self) -> Span {
self.span
}
fn set_span(&mut self, span: Span) {
self.span = span;
}
fn boxed(self) -> Box<Self> {
Box::new(self)
}
fn get_redirs(&self) -> &Vec<Box<dyn Node>> {
&self.redirs
}
fn push_redir(&mut self, redir: Box<dyn Node>) {
self.redirs.push(redir)
}
fn as_any(&self) -> &dyn Any {
self
}
fn tokens(&self) -> &VecDeque<Tk> {
&self.tokens
}
fn flags(&self) -> NdFlags {
self.flags
}
fn mod_flags(&mut self, transform: Box<dyn FnOnce(&mut NdFlags)>) {
transform(&mut self.flags)
}
}
};
TokenStream::from(expanded)
}

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -0,0 +1,716 @@
use std::collections::VecDeque;
use std::ffi::{CString, OsStr};
use std::fs;
use std::os::fd::AsRawFd;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::{FileTypeExt, MetadataExt};
use std::path::{Path, PathBuf};
use bitflags::bitflags;
use libc::{getegid, geteuid};
use log::info;
use nix::fcntl::OFlag;
use nix::sys::stat::Mode;
use nix::sys::wait::{waitpid, WaitStatus};
use nix::unistd::{access, fork, isatty, setpgid, AccessFlags, ForkResult};
use crate::execute::{ProcIO, RshWaitStatus, RustFd};
use crate::interp::parse::{NdFlags, NdType, Node, Span};
use crate::interp::{expand, token};
use crate::interp::token::{Redir, RedirType, Tk, TkType};
use crate::shellenv::{EnvFlags, JobFlags, ShellEnv};
use crate::event::ShellError;
pub const BUILTINS: [&str; 14] = [
"echo", "set", "shift", "export", "cd", "readonly", "declare", "local", "unset", "trap", "node",
"exec", "source", "wait",
];
bitflags! {
#[derive(Debug)]
pub struct EchoFlags: u8 {
const USE_ESCAPE = 0b0001;
const NO_NEWLINE = 0b0010;
const NO_ESCAPE = 0b0100;
const STDERR = 0b1000;
}
}
fn open_file_descriptors(redirs: VecDeque<Node>) -> Result<Vec<RustFd>, ShellError> {
let mut fd_stack: Vec<RustFd> = Vec::new();
let mut fd_dupes: Vec<(i32,i32)> = Vec::new();
info!("Handling redirections for builtin: {:?}", redirs);
for redir_tk in redirs {
if let NdType::Redirection { ref redir } = redir_tk.nd_type {
let Redir { fd_source, op, fd_target, file_target } = redir;
if let Some(target) = fd_target {
fd_dupes.push((*target,*fd_source));
} else if let Some(file_path) = file_target {
let source_fd = RustFd::new(*fd_source)?;
let flags = match op {
RedirType::Input => OFlag::O_RDONLY,
RedirType::Output => OFlag::O_WRONLY | OFlag::O_CREAT | OFlag::O_TRUNC,
RedirType::Append => OFlag::O_WRONLY | OFlag::O_CREAT | OFlag::O_APPEND,
_ => unimplemented!("Heredocs and herestrings are not implemented yet."),
};
let mut file_fd = RustFd::open(Path::new(file_path.text()), flags, Mode::from_bits(0o644).unwrap())?;
file_fd.dup2(&source_fd)?;
file_fd.close()?;
fd_stack.push(source_fd);
}
}
}
while let Some((target,source)) = fd_dupes.pop() {
let mut target_fd = RustFd::new(target)?;
let source_fd = RustFd::new(source)?;
target_fd.dup2(&source_fd)?;
target_fd.close()?;
fd_stack.push(source_fd);
}
Ok(fd_stack)
}
pub fn catstr(mut c_strings: VecDeque<CString>,newline: bool) -> CString {
let mut cat: Vec<u8> = vec![];
let newline_bytes = b"\n\0";
let space_bytes = b" ";
while let Some(c_string) = c_strings.pop_front() {
let bytes = c_string.to_bytes_with_nul();
if c_strings.is_empty() {
// Last string: include the null terminator
cat.extend_from_slice(&bytes[..bytes.len() - 1]);
} else {
// Intermediate strings: exclude the null terminator and add whitespace
cat.extend_from_slice(&bytes[..bytes.len() - 1]);
cat.extend_from_slice(space_bytes);
}
}
if newline {
cat.extend_from_slice(newline_bytes);
} else {
cat.extend_from_slice(b"\0");
}
CString::from_vec_with_nul(cat).unwrap()
}
/// Performs a test on a single argument by transforming it and then applying a property check.
///
/// This function takes a mutable reference to a `VecDeque` of `String` arguments, a transformation function,
/// and a property function. It pops the front element from the `VecDeque`, applies the transformation
/// function to it, and then checks the transformed value against the property function.
///
/// # Arguments
///
/// * `args` - A mutable reference to a `VecDeque` of `String` arguments.
/// * `transform` - A function that takes a `String` and returns a `Result<T, ShellError>`.
/// * `property` - A function that takes a reference to `T` and returns a `bool`.
///
/// # Returns
///
/// * `Ok(bool)` - If the transformation and property check are successful, returns the result of the property check.
/// * `Err(ShellError)` - If the `VecDeque` is empty or the transformation fails, returns an appropriate `ShellError`.
///
/// # Example
///
/// ```
/// let mut args = VecDeque::new();
/// args.push_back("42".to_string());
/// let transform = |s: String| -> Result<i32, ShellError> { Ok(s.parse::<i32>().unwrap()) };
/// let property = |x: &i32| -> bool { *x > 0 };
/// let result = do_test(&mut args, transform, property);
/// assert!(result.unwrap());
/// ```
fn do_test<T, F1, F2>(
args: &mut VecDeque<Tk>,
transform: F1,
property: F2,
span: Span
) -> Result<bool, ShellError>
where
F1: FnOnce(Tk) -> Result<T, ShellError>,
F2: FnOnce(&T) -> bool
{
if let Some(st) = args.pop_front() {
let transformed = transform(st).map_err(|_| false);
if transformed.is_err() {
return Ok(false)
}
Ok(property(&transformed.unwrap()))
} else {
Err(ShellError::from_syntax("Did not find a comparison target for integer in test", span))
}
}
/// Compares two arguments by transforming them and then applying a comparison function.
///
/// This function takes a `String` argument, a mutable reference to a `VecDeque` of `String` arguments,
/// a transformation function, and a comparison function. It pops the front element from the `VecDeque`,
/// applies the transformation function to both the provided `String` and the popped element, and then
/// compares the transformed values using the comparison function.
///
/// # Arguments
///
/// * `arg1` - The first `String` argument to be transformed and compared.
/// * `args` - A mutable reference to a `VecDeque` of `String` arguments.
/// * `transform` - A function that takes a `String` and returns a `Result<T, ShellError>`.
/// * `comparison` - A function that takes two references to `T` and returns a `bool`.
///
/// # Returns
///
/// * `Ok(bool)` - If the transformation and comparison are successful, returns the result of the comparison.
/// * `Err(ShellError)` - If the `VecDeque` is empty or the transformation fails, returns an appropriate `ShellError`.
///
/// # Example
///
/// ```
/// let mut args = VecDeque::new();
/// args.push_back("42".to_string());
/// let transform = |s: String| -> Result<i32, ShellError> { Ok(s.parse::<i32>().unwrap()) };
/// let comparison = |x: &i32, y: &i32| -> bool { x == y };
/// let result = do_cmp("42".to_string(), &mut args, transform, comparison);
/// assert!(result.unwrap());
/// ```
fn do_cmp<T, F1, F2>(
arg1: Tk,
args: &mut VecDeque<Tk>,
transform: F1,
comparison: F2
) -> Result<bool, ShellError>
where
F1: Fn(Tk) -> Result<T, ShellError>,
F2: FnOnce(&T, &T) -> bool
{
if let Some(st) = args.pop_front() {
let left = transform(arg1).map_err(|_| false);
let right = transform(st).map_err(|_| false);
if left.is_err() || right.is_err() {
return Ok(false)
}
Ok(comparison(&left.unwrap(), &right.unwrap()))
} else {
Err(ShellError::from_syntax("Did not find a comparison target for integer in test", arg1.span()))
}
}
/// Performs a logical operation (AND or OR) on two boolean results.
///
/// This function takes a mutable reference to a `VecDeque` of `String` arguments, the command (`test` or `[`),
/// the first result of a test, and the logical operator (`-a` for AND, `-o` for OR). It recursively calls the `test`
/// function to evaluate the next condition and combines the results using the specified logical operator.
///
/// # Arguments
///
/// * `args` - A mutable reference to a `VecDeque` of `String` arguments.
/// * `command` - The command (`test` or `[`).
/// * `result1` - The result of the first test.
/// * `operator` - The logical operator (`-a` for AND, `-o` for OR).
///
/// # Returns
///
/// * `Result<bool, ShellError>` - Returns the combined result of the logical operation.
///
/// # Errors
///
/// * Returns `ShellError::from_syntax` if there is a syntax error or missing arguments.
fn do_logical_op(
args: &mut VecDeque<Tk>,
command: Tk,
result1: bool,
operator: Tk
) -> Result<bool, ShellError> {
args.push_front(command);
let result2 = test(std::mem::take(args)).map(|res| matches!(res,RshWaitStatus::Success {..}))?;
match operator.text() {
"!" => { Ok(!result2) },
"-a" => { Ok(result1 && result2) },
"-o" => { Ok(result1 || result2) },
_ => Err(ShellError::from_syntax("Expected a logical operator (-a or -o)", operator.span())),
}
}
/// Implements the `test` builtin command for a Unix shell.
///
/// The `test` command evaluates conditional expressions and returns a success or failure status based on the evaluation.
/// This function handles various types of tests, including file attributes, string comparisons, and integer comparisons.
///
/// # Arguments
///
/// * `argv` - A vector of `CString` arguments passed to the `test` command.
///
/// # Returns
///
/// * `Ok(RshWaitStatus)` - Returns a success or failure status based on the evaluation of the conditional expression.
/// * `Err(ShellError)` - Returns an appropriate `ShellError` if there is a syntax error or other issues with the arguments.
///
/// # Examples
///
/// ```
/// let argv = vec![CString::new("test").unwrap(), CString::new("-f").unwrap(), CString::new("file.txt").unwrap()];
/// let result = test(argv);
/// assert!(result.is_ok());
/// ```
///
/// ```
/// let argv = vec![CString::new("[").unwrap(), CString::new("1").unwrap(), CString::new("-eq").unwrap(), CString::new("1").unwrap(), CString::new("]").unwrap()];
/// let result = test(argv);
/// assert!(result.is_ok());
/// ```
///
/// # Supported Tests
///
/// ## File Tests
///
/// - `-b`: Block device
/// - `-c`: Character device
/// - `-d`: Directory
/// - `-e`: File exists
/// - `-f`: Regular file
/// - `-g`: Set-group-ID flag set
/// - `-G`: Group-ID matches
/// - `-h`: Symbolic link
/// - `-L`: Symbolic link (same as `-h`)
/// - `-k`: Sticky bit set
/// - `-N`: Modified since last read
/// - `-O`: User-ID matches
/// - `-p`: Named pipe
/// - `-r`: Readable
/// - `-s`: File size greater than zero
/// - `-S`: Socket
/// - `-u`: Set-user-ID flag set
/// - `-w`: Writable
/// - `-x`: Executable
///
/// ## String Tests
///
/// - `-n`: String is non-empty
/// - `-z`: String is empty
/// - `=`: Strings are equal
/// - `!=`: Strings are not equal
///
/// ## Integer Tests
///
/// - `-eq`: Equal
/// - `-ge`: Greater than or equal
/// - `-gt`: Greater than
/// - `-le`: Less than or equal
/// - `-lt`: Less than
/// - `-ne`: Not equal
///
/// ## Logical Operators
///
/// - `-a`: And
/// - `-o`: Or
///
/// ## Special Operators
///
/// - `!`: Not
/// - `-t`: File descriptor is associated with a terminal
///
/// # Notes
///
/// - The `test` command can be invoked using the `test` keyword or with square brackets `[ ]`.
/// - The function handles various error conditions, such as missing arguments or invalid syntax.
///
/// # Errors
///
/// - Returns `ShellError::from_syntax` if there is a syntax error or missing arguments.
/// - Returns `ShellError::from_syntax` if an expected comparison operator is missing.
///
/// # Safety
///
/// This function uses unsafe code to get the effective user ID and group ID using `geteuid` and `getegid`.
///
/// # Panics
///
/// This function does not panic.
pub fn test(mut argv: VecDeque<Tk>) -> Result<RshWaitStatus, ShellError> {
info!("Starting builtin test");
let span = Span::from(argv.front().unwrap().span().start,argv.back().unwrap().span().end);
let is_false = || -> Result<RshWaitStatus, ShellError> { Ok(RshWaitStatus::Fail { code: 1, cmd: Some("test".into()), span }) };
let is_true = || -> Result<RshWaitStatus, ShellError> { Ok(RshWaitStatus::Success { span }) };
let is_int = |tk: &Tk| -> bool { tk.text().parse::<i32>().is_ok() };
let to_int = |tk: Tk| -> Result<i32, ShellError> {
tk.text().parse::<i32>().map_err(|_| ShellError::from_syntax("Expected an integer in test", tk.span()))
};
let is_path = |tk: &Tk| -> bool { PathBuf::from(tk.text()).exists() };
let to_meta = |tk: Tk| -> Result<fs::Metadata, ShellError> {
fs::metadata(tk.text()).map_err(|_| ShellError::from_syntax(&format!("Test: Path '{}' does not exist", tk.text()), tk.span()))
};
let string_noop = |tk: Tk| -> Result<String, ShellError> { Ok(tk.text().into()) };
let command = argv.pop_front().unwrap();
let is_bracket = match command.text() {
"[" => true,
"test" => false,
_ => unreachable!()
};
if is_bracket {
if let Some(arg) = argv.back() {
if arg.text() != "]" {
return Err(ShellError::from_syntax("Test is missing a closing bracket", arg.span()))
}
} else {
return Err(ShellError::from_syntax("Found a test with no arguments", command.span()))
}
}
if let Some(arg) = argv.pop_front() {
let result1 = match arg.text() {
"!" => do_logical_op(&mut argv, command.clone(), true, arg)?,
"-t" => do_test(&mut argv, to_int, |int| isatty(*int).is_ok(), arg.span())?,
"-b" => do_test(&mut argv, to_meta, |meta| meta.file_type().is_block_device(), arg.span())?,
"-c" => do_test(&mut argv, to_meta, |meta| meta.file_type().is_char_device(), arg.span())?,
"-d" => do_test(&mut argv, to_meta, |meta| meta.is_dir(), arg.span())?,
"-f" => do_test(&mut argv, to_meta, |meta| meta.is_file(), arg.span())?,
"-g" => do_test(&mut argv, to_meta, |meta| meta.mode() & 0o2000 != 0, arg.span())?,
"-G" => do_test(&mut argv, to_meta, |meta| meta.gid() == unsafe { getegid() }, arg.span())?,
"-h" => do_test(&mut argv, to_meta, |meta| meta.is_symlink(), arg.span())?,
"-L" => do_test(&mut argv, to_meta, |meta| meta.is_symlink(), arg.span())?,
"-k" => do_test(&mut argv, to_meta, |meta| meta.mode() & 0o1000 != 0, arg.span())?,
"-N" => do_test(&mut argv, to_meta, |meta| meta.mtime() > meta.atime(), arg.span())?,
"-O" => do_test(&mut argv, to_meta, |meta| meta.uid() == unsafe { geteuid() }, arg.span())?,
"-p" => do_test(&mut argv, to_meta, |meta| meta.file_type().is_fifo(), arg.span())?,
"-s" => do_test(&mut argv, to_meta, |meta| meta.len() > 0, arg.span())?,
"-S" => do_test(&mut argv, to_meta, |meta| meta.file_type().is_socket(), arg.span())?,
"-u" => do_test(&mut argv, to_meta, |meta| meta.mode() & 0o4000 != 0, arg.span())?,
"-n" => do_test(&mut argv, string_noop, |st| !st.is_empty(), arg.span())?,
"-z" => do_test(&mut argv, string_noop, |st| st.is_empty(), arg.span())?,
"-e" => do_test(&mut argv, string_noop, |st| Path::new(st).exists(), arg.span())?,
"-r" => do_test(&mut argv, string_noop, |st| access(Path::new(st), AccessFlags::R_OK).is_ok(), arg.span())?,
"-w" => do_test(&mut argv, string_noop, |st| access(Path::new(st), AccessFlags::W_OK).is_ok(), arg.span())?,
"-x" => do_test(&mut argv, string_noop, |st| access(Path::new(st), AccessFlags::X_OK).is_ok(), arg.span())?,
_ if is_int(&arg) => {
if let Some(cmp) = argv.pop_front() {
match cmp.text() {
"-eq" => do_cmp(arg, &mut argv, to_int, |left, right| left == right)?,
"-ge" => do_cmp(arg, &mut argv, to_int, |left, right| left >= right)?,
"-gt" => do_cmp(arg, &mut argv, to_int, |left, right| left > right)?,
"-le" => do_cmp(arg, &mut argv, to_int, |left, right| left <= right)?,
"-lt" => do_cmp(arg, &mut argv, to_int, |left, right| left < right)?,
"-ne" => do_cmp(arg, &mut argv, to_int, |left, right| left != right)?,
_ => {
return Err(ShellError::from_syntax("Expected an integer after comparison operator in test", cmp.span()));
}
}
} else {
return Err(ShellError::from_syntax("Expected a comparison operator after integer in test", command.span()));
}
}
_ if is_path(&arg) && argv.front().is_some_and(|arg| matches!(arg.text(), "-ef" | "-nt" | "-ot")) => {
if let Some(cmp) = argv.pop_front() {
match cmp.text() {
"-ef" => do_cmp(arg, &mut argv, to_meta, |left, right| left.dev() == right.dev() && left.ino() == right.ino())?,
"-nt" => do_cmp(arg, &mut argv, to_meta, |left, right| left.mtime() > right.mtime())?,
"-ot" => do_cmp(arg, &mut argv, to_meta, |left, right| left.mtime() < right.mtime())?,
_ => {
return Err(ShellError::from_syntax("Expected a file name after -b flag", cmp.span()));
}
}
} else {
return Err(ShellError::from_syntax("Expected a comparison operator after path name in test", command.span()));
}
}
_ => {
if argv.is_empty() {
!arg.text().is_empty() // Default behavior is to return true if a string is not empty
} else if let Some(cmp) = argv.pop_front() {
match cmp.text() {
"=" => do_cmp(arg, &mut argv, string_noop, |left, right| left == right)?,
"!=" => do_cmp(arg, &mut argv, string_noop, |left, right| left != right)?,
_ => {
if cmp.text() == "==" {
return Err(ShellError::from_syntax("`==` is not a valid comparison operator, use `=` instead", cmp.span()));
} else {
return Err(ShellError::from_syntax("Expected a comparison operator after string in test", cmp.span()));
}
}
}
} else {
return Err(ShellError::from_syntax("Expected a comparison operator after string in test", command.span()));
}
}
};
if let Some(arg) = argv.pop_front() {
match arg.text() {
"-a" | "-o" => { // And/Or
let result = do_logical_op(&mut argv, command, result1, arg)?;
match result {
true => return is_true(),
false => return is_false(),
}
}
"]" => {}
_ => {
return Err(ShellError::from_syntax("Unexpected extra argument found in test", arg.span()));
}
}
}
match result1 {
true => is_true(),
false => is_false(),
}
} else {
Err(ShellError::from_syntax("Test called with no arguments", command.span()))
}
}
pub fn alias(shellenv: &mut ShellEnv, node: Node) -> Result<RshWaitStatus, ShellError> {
let mut argv: VecDeque<Tk> = node.get_argv()?.into();
argv.pop_front();
while let Some(arg) = argv.pop_front() {
if !token::REGEX["assignment"].is_match(arg.text()) {
return Err(ShellError::from_syntax(&format!("Expected an assignment pattern in alias args, got {}",arg.text()), arg.span()))
}
let (alias,value) = arg.text().split_once('=').unwrap();
if let Err(e) = shellenv.set_alias(alias.into(), value.into()) {
return Err(ShellError::from_parse(e.as_str(), node.span()))
}
}
Ok(RshWaitStatus::Success { span: node.span() })
}
pub fn cd(shellenv: &mut ShellEnv, node: Node) -> Result<RshWaitStatus,ShellError> {
let mut argv = node
.get_argv()?
.iter()
.map(|arg| CString::new(arg.text()).unwrap())
.collect::<VecDeque<CString>>();
argv.pop_front();
let new_pwd;
if let Some(arg) = argv.pop_front() {
new_pwd = arg;
} else if let Some(home_path) = shellenv.get_variable("HOME") {
new_pwd = CString::new(home_path).unwrap();
} else {
new_pwd = CString::new("/").unwrap();
}
let path = Path::new(new_pwd.to_str().unwrap());
shellenv.change_dir(path, node.span())?;
Ok(RshWaitStatus::Success { span: node.span() })
}
pub fn source(shellenv: &mut ShellEnv, node: Node) -> Result<RshWaitStatus,ShellError> {
let mut argv = node
.get_argv()?
.iter()
.map(|arg| CString::new(arg.text()).unwrap())
.collect::<VecDeque<CString>>();
argv.pop_front();
for path in argv {
let file_path = Path::new(OsStr::from_bytes(path.as_bytes()));
shellenv.source_file(file_path.to_path_buf())?
}
Ok(RshWaitStatus::Success { span: node.span() })
}
fn flags_from_chars(chars: &str) -> EnvFlags {
let flag_list = chars.chars();
let mut env_flags = EnvFlags::empty();
for ch in flag_list {
match ch {
'a' => env_flags |= EnvFlags::EXPORT_ALL_VARS,
'b' => env_flags |= EnvFlags::REPORT_JOBS_ASAP,
'e' => env_flags |= EnvFlags::EXIT_ON_ERROR,
'f' => env_flags |= EnvFlags::NO_GLOB,
'h' => env_flags |= EnvFlags::HASH_CMDS,
'k' => env_flags |= EnvFlags::ASSIGN_ANYWHERE,
'm' => env_flags |= EnvFlags::ENABLE_JOB_CTL,
'n' => env_flags |= EnvFlags::NO_EXECUTE,
'r' => env_flags |= EnvFlags::ENABLE_RSHELL,
't' => env_flags |= EnvFlags::EXIT_AFTER_EXEC,
'u' => env_flags |= EnvFlags::UNSET_IS_ERROR,
'v' => env_flags |= EnvFlags::PRINT_INPUT,
'x' => env_flags |= EnvFlags::STACK_TRACE,
'B' => env_flags |= EnvFlags::EXPAND_BRACES,
'C' => env_flags |= EnvFlags::NO_OVERWRITE,
'E' => env_flags |= EnvFlags::INHERIT_ERR,
'H' => env_flags |= EnvFlags::HIST_SUB,
'P' => env_flags |= EnvFlags::NO_CD_SYMLINKS,
'T' => env_flags |= EnvFlags::INHERIT_RET,
_ => eprintln!("set: no such option '{}'",ch)
}
}
env_flags
}
pub fn set_or_unset(shellenv: &mut ShellEnv, node: Node, set: bool) -> Result<RshWaitStatus,ShellError> {
let span = node.span();
if let NdType::Builtin { mut argv } = node.nd_type {
argv.pop_front(); // Ignore 'set'
if argv.front().is_none_or(|arg| !arg.text().starts_with('-')) {
return Err(ShellError::from_execf("Invalid 'set' invocation", 1, span))
}
let flag_arg = argv.pop_front().unwrap();
let set_flags = flag_arg.text();
let set_flags = set_flags.strip_prefix('-').unwrap();
let env_flags = flags_from_chars(set_flags);
match set {
true => shellenv.set_flags(env_flags),
false => shellenv.unset_flags(env_flags),
}
Ok(RshWaitStatus::new())
} else { unreachable!() }
}
pub fn pwd(shellenv: &ShellEnv, span: Span) -> Result<RshWaitStatus, ShellError> {
if let Some(pwd) = shellenv.get_variable("PWD") {
let stdout = RustFd::from_stdout()?;
stdout.write(pwd.as_bytes())?;
Ok(RshWaitStatus::Success { span })
} else {
Err(ShellError::from_execf("PWD environment variable is unset", 1, span))
}
}
pub fn export(shellenv: &mut ShellEnv, node: Node) -> Result<RshWaitStatus, ShellError> {
let last_status = RshWaitStatus::Success { span: node.span() };
if let NdType::Builtin { mut argv } = node.nd_type {
argv.pop_front(); // Ignore "export"
while let Some(ass) = argv.pop_front() {
if let Some((key,value)) = ass.text().split_once('=') {
shellenv.export_variable(key.to_string(), value.to_string());
} else {
return Err(ShellError::from_execf(format!("Expected a variable assignment in export, got this: {}", ass.text()).as_str(), 1, ass.span()))
}
}
Ok(last_status)
} else { unreachable!() }
}
pub fn jobs(shellenv: &mut ShellEnv, node: Node, mut io: ProcIO, in_pipe: bool) -> Result<RshWaitStatus,ShellError> {
let mut argv = node.get_argv()?.into_iter().collect::<VecDeque<Tk>>();
argv.pop_front();
let mut flags = JobFlags::empty();
while let Some(arg) = argv.pop_front() {
let mut chars = arg.text().chars().collect::<VecDeque<char>>();
if chars.front().is_none_or(|ch| *ch != '-') {
return Err(ShellError::from_execf("Invalid flag in `jobs` invocation", 1, node.span()))
}
chars.pop_front(); // Ignore the leading hyphen
while let Some(ch) = chars.pop_front() {
let flag = match ch {
'l' => JobFlags::LONG,
'p' => JobFlags::PIDS_ONLY,
'n' => JobFlags::NEW_ONLY,
'r' => JobFlags::RUNNING,
's' => JobFlags::STOPPED,
_ => return Err(ShellError::from_execf("Invalid flag in `jobs` invocation", 1, node.span()))
};
flags |= flag;
}
}
shellenv.job_table.print_jobs(&flags);
Ok(RshWaitStatus::Success { span: node.span() })
}
pub fn echo(shellenv: &mut ShellEnv, node: Node, mut io: ProcIO, in_pipe: bool) -> Result<RshWaitStatus, ShellError> {
let mut flags = EchoFlags::empty();
let span = node.span();
let mut argv = node.get_argv()?.into_iter().collect::<VecDeque<Tk>>();
argv.pop_front(); // Remove 'echo' from argv
// Get flags
if argv.front().is_some_and(|arg| arg.text().starts_with('-')) {
let next_arg = argv.pop_front().unwrap();
let mut options = next_arg.text().strip_prefix('-').unwrap().chars();
loop {
match options.next() {
Some('e') => {
if flags.contains(EchoFlags::NO_ESCAPE) {
flags &= !EchoFlags::NO_ESCAPE
}
flags |= EchoFlags::USE_ESCAPE
}
Some('r') => flags |= EchoFlags::STDERR,
Some('n') => flags |= EchoFlags::NO_NEWLINE,
Some('E') => {
if flags.contains(EchoFlags::USE_ESCAPE) {
flags &= !EchoFlags::USE_ESCAPE
}
flags |= EchoFlags::NO_ESCAPE
}
_ => break
}
}
if flags.is_empty() {
argv.push_front(next_arg);
}
}
let argv = argv.into_iter().map(|tk| {
let text = tk.text();
if flags.contains(EchoFlags::USE_ESCAPE) {
CString::new(expand::process_ansi_escapes(text)).unwrap()
} else {
CString::new(tk.text()).unwrap()
}
}).collect::<VecDeque<CString>>();
let redirs = node.get_redirs()?;
info!("Executing echo with argv: {:?}, and redirs: {:?}", argv,node.redirs);
io.backup_fildescs()?;
let newline_opt = !flags.contains(EchoFlags::NO_NEWLINE);
let output_str = catstr(argv, newline_opt);
let mut fd_stack = vec![];
fd_stack.extend(open_file_descriptors(redirs.into())?);
let output_fd = if flags.contains(EchoFlags::STDERR) {
if let Some(ref err_fd) = io.stderr {
err_fd.lock().unwrap().dup().unwrap_or_else(|_| RustFd::from_stderr().unwrap())
} else {
RustFd::from_stderr()?
}
} else if let Some(ref out_fd) = io.stdout {
out_fd.lock().unwrap().dup().unwrap_or_else(|_| RustFd::from_stdout().unwrap())
} else {
RustFd::from_stdout()?
};
if let Some(ref fd) = io.stderr {
if !flags.contains(EchoFlags::STDERR) {
let fd = fd.lock().unwrap();
output_fd.dup2(&fd.as_raw_fd())?;
}
}
if in_pipe {
output_fd.write(output_str.as_bytes())?;
std::process::exit(0);
}
match unsafe { fork() } {
Ok(ForkResult::Child) => {
output_fd.write(output_str.as_bytes())?;
std::process::exit(0);
}
Ok(ForkResult::Parent { child }) => {
for mut fd in fd_stack {
fd.close().unwrap();
}
io.restore_fildescs()?;
if node.flags.contains(NdFlags::BACKGROUND) {
setpgid(child, child).map_err(|_| ShellError::from_io())?;
shellenv.new_job(vec![child], vec!["echo".into()], child);
Ok(RshWaitStatus::Success { span })
} else {
match waitpid(child, None) {
Ok(WaitStatus::Exited(_, code)) => match code {
0 => Ok(RshWaitStatus::Success { span }),
_ => Ok(RshWaitStatus::Fail { code, cmd: Some("echo".into()), span }),
},
Ok(WaitStatus::Signaled(_,signal,_)) => {
Ok(RshWaitStatus::Fail { code: 128 + signal as i32, cmd: Some("echo".into()), span })
}
Ok(_) => Err(ShellError::from_execf("Unexpected waitpid result", 1, span)),
Err(err) => Err(ShellError::from_execf(&format!("Waitpid failed: {}", err), 1, span)),
}
}
}
Err(_) => Err(ShellError::from_execf("Fork failed", 1, span)),
}
}

View File

@@ -0,0 +1,169 @@
use crossterm::{
cursor::{MoveTo, RestorePosition, Show}, execute, style::Print, terminal::{disable_raw_mode, enable_raw_mode, size, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen}
};
use log::info;
use rustyline::{completion::{Candidate, Completer, FilenameCompleter}, error::ReadlineError, history::{FileHistory, History}, Context, Helper, Highlighter, Hinter, Validator};
use skim::{prelude::{SkimItemReader, SkimOptionsBuilder}, Skim};
use std::{collections::HashSet, env, io::stdout, path::PathBuf};
use crate::{interp::expand, shellenv::ShellEnv};
#[derive(Hinter,Highlighter,Validator,Helper)]
pub struct RshHelper<'a> {
filename_comp: FilenameCompleter,
commands: Vec<String>, // List of built-in or cached commands
shellenv: &'a ShellEnv
}
impl<'a> RshHelper<'a> {
pub fn new(shellenv: &'a ShellEnv) -> Self {
// Prepopulate some built-in commands (could also load dynamically)
let commands = vec![
"cd".to_string(),
"ls".to_string(),
"echo".to_string(),
"exit".to_string(),
];
let mut helper = RshHelper {
filename_comp: FilenameCompleter::new(),
commands,
shellenv
};
helper.update_commands_from_path();
helper
}
// Dynamically add commands (if needed, e.g., external binaries in $PATH)
fn update_commands_from_path(&mut self) {
if let Some(paths) = env::var_os("PATH") {
let mut external_commands = HashSet::new();
for path in env::split_paths(&paths) {
if let Ok(entries) = std::fs::read_dir(path) {
for entry in entries.flatten() {
if let Ok(file_name) = entry.file_name().into_string() {
external_commands.insert(file_name);
}
}
}
}
self.commands.extend(external_commands);
}
}
}
impl<'a> Completer for RshHelper<'a> {
type Candidate = String;
fn complete(
&self,
line: &str,
pos: usize,
_: &Context<'_>,
) -> Result<(usize, Vec<Self::Candidate>), ReadlineError> {
let mut completions = Vec::new();
let num_words = line.split_whitespace().count();
// Determine if this is a file path or a command completion
if !line.is_empty() && num_words > 1 {
let hist_path = expand::expand_var(self.shellenv, "$HIST_FILE".into());
info!("history path in init_prompt(): {}", hist_path);
let hist_path = PathBuf::from(hist_path);
// Delegate to FilenameCompleter for file path completion
let mut history = FileHistory::new();
history.load(&hist_path).unwrap();
let (start, matches) = self.filename_comp.complete(line, pos, &Context::new(&history))?;
completions.extend(matches.iter().map(|c| c.display().to_string()));
// Invoke fuzzyfinder if there are matches
if !completions.is_empty() && completions.len() > 1 {
if let Some(selected) = skim_comp(completions.clone()) {
return Ok((start, vec![selected]));
}
}
return Ok((start, completions));
}
// Command completion
let prefix = &line[..pos]; // The part of the line to match
completions.extend(
self.commands
.iter()
.filter(|cmd| cmd.starts_with(prefix)) // Match prefix
.cloned(), // Clone matched command names
);
// Invoke fuzzyfinder if there are matches
if completions.len() > 1 {
if let Some(selected) = skim_comp(completions.clone()) {
return Ok((0, vec![selected]));
}
}
// Return completions, starting from the beginning of the word
Ok((0, completions))
}
}
pub fn skim_comp(options: Vec<String>) -> Option<String> {
let mut stdout = stdout();
// Enter the alternate screen
execute!(stdout, EnterAlternateScreen).unwrap();
enable_raw_mode().unwrap();
// Get terminal dimensions
let (cols, rows) = size().unwrap();
let width = cols.min(50); // Set floating window width
let height = rows.min(10); // Set floating window height
let start_col = 0;
let start_row = (rows - height) / 2;
// Draw the floating window border
execute!(
stdout,
MoveTo(start_col, start_row),
Print("".to_string() + &"".repeat(width as usize - 2) + "")
)
.unwrap();
for i in 1..height - 1 {
execute!(
stdout,
MoveTo(start_col, start_row + i),
Print("".to_string() + &" ".repeat(width as usize - 2) + "")
)
.unwrap();
}
execute!(
stdout,
MoveTo(start_col, start_row + height - 1),
Print("".to_string() + &"".repeat(width as usize - 2) + "")
)
.unwrap();
// Prepare options for skim
let options_join = options.join("\n");
let input = SkimItemReader::default().of_bufread(std::io::Cursor::new(options_join));
let skim_options = SkimOptionsBuilder::default()
.prompt("Select > ".to_string())
.height("25%".to_string()) // Height in lines relative to floating window
.multi(false)
.build()
.unwrap();
// Run skim within the alternate screen
let selected = Skim::run_with(&skim_options, Some(input))
.and_then(|out| out.selected_items.first().cloned())
.map(|item| item.output().to_string());
// Leave the alternate screen and restore original content
execute!(stdout, Clear(ClearType::All), RestorePosition, LeaveAlternateScreen, Show).unwrap();
disable_raw_mode().unwrap();
selected
}

View File

@@ -0,0 +1,522 @@
use std::{fmt, io};
use std::os::fd::BorrowedFd;
use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus};
use nix::unistd::Pid;
use tokio::sync::mpsc;
use log::{error,debug,info};
use tokio::signal::unix::{signal, Signal, SignalKind};
use crate::execute::RshWaitStatus;
use crate::interp::parse::{Node, Span};
use crate::{execute, prompt};
use crate::shellenv::ShellEnv;
#[derive(Debug, PartialEq)]
pub enum ShellError {
CommandNotFound(String, Span),
InvalidSyntax(String, Span),
ParsingError(String, Span),
ExecFailed(String, i32, Span),
IoError(String),
InternalError(String),
}
impl ShellError {
pub fn from_io() -> Self {
let err = io::Error::last_os_error();
ShellError::IoError(err.to_string())
}
pub fn from_execf(msg: &str, code: i32, span: Span) -> Self {
ShellError::ExecFailed(msg.to_string(), code, span)
}
pub fn from_parse(msg: &str, span: Span) -> Self {
ShellError::ParsingError(msg.to_string(), span)
}
pub fn from_syntax(msg: &str, span: Span) -> Self {
ShellError::InvalidSyntax(msg.to_string(), span)
}
pub fn from_no_cmd(msg: &str, span: Span) -> Self {
ShellError::CommandNotFound(msg.to_string(), span)
}
pub fn from_internal(msg: &str) -> Self {
ShellError::InternalError(msg.to_string())
}
// This is used in the context of functions
// To prevent the error from trying to use the span
// Of the offending command that is inside of the function
pub fn overwrite_span(&self, new_span: Span) -> Self {
match self {
ShellError::IoError(err) => ShellError::IoError(err.to_string()),
ShellError::CommandNotFound(msg,_) => ShellError::CommandNotFound(msg.to_string(),new_span),
ShellError::InvalidSyntax(msg,_) => ShellError::InvalidSyntax(msg.to_string(),new_span),
ShellError::ParsingError(msg,_) => ShellError::ParsingError(msg.to_string(),new_span),
ShellError::ExecFailed(msg,code,_) => ShellError::ExecFailed(msg.to_string(),*code,new_span),
ShellError::InternalError(msg) => ShellError::InternalError(msg.to_string()),
}
}
pub fn is_fatal(&self) -> bool {
match self {
ShellError::IoError(..) => true,
ShellError::CommandNotFound(..) => false,
ShellError::ExecFailed(..) => false,
ShellError::ParsingError(..) => false,
ShellError::InvalidSyntax(..) => false,
ShellError::InternalError(..) => false,
}
}
}
/// A custom error type for the shell program that provides detailed error context.
/// This struct encapsulates the original input and the error, and offers methods
/// to format the error context for more informative output.
pub struct ShellErrorFull {
/// The original input string that led to the error.
input: String,
/// The shell error encountered during execution.
error: ShellError,
}
impl ShellErrorFull {
/// Creates a new `ShellErrorFull` instance from the given input and error.
///
/// # Arguments
///
/// * `input` - The original input string that caused the error.
/// * `error` - The `ShellError` encountered during execution.
///
/// # Returns
///
/// A `ShellErrorFull` instance encapsulating the input and error.
pub fn from(input: String, error: ShellError) -> Self {
Self { input, error }
}
/// Formats and prints the error context, including the line, column, offending input,
/// and a pointer indicating the error location.
///
/// # Arguments
///
/// * `span` - The span indicating the start and end positions of the error in the input.
fn format_error_context(&self, span: Option<Span>) {
if let Some(span) = span {
let (line, col) = Self::get_line_col(&self.input, span.start);
let (window, window_offset) = Self::generate_window(&self.input, line, col);
let span_diff = span.end - span.start;
let pointer = Self::get_pointer(span_diff, window_offset);
println!();
println!("{};{}:", line + 1, col + 1);
println!("{}", window);
println!("{}", pointer);
}
}
/// Generates a string pointer indicating the location of the error in the input.
/// This pointer is composed of a caret (^) and tildes (~) to mark the span.
///
/// # Arguments
///
/// * `span_diff` - The length of the error span.
/// * `offset` - The offset position of the error within the window.
///
/// # Returns
///
/// A string representing the pointer to the error location.
fn get_pointer(span_diff: usize, offset: usize) -> String {
let padding = " ".repeat(offset);
let visible_span = span_diff.min(40 - offset);
let mut pointer = String::new();
pointer.push('^');
if visible_span > 1 {
pointer.push_str(&"~".repeat(visible_span - 2));
pointer.push('^');
}
format!("{}{}", padding, pointer)
}
/// Determines the line and column number of a given offset within the input string.
///
/// # Arguments
///
/// * `input` - The original input string.
/// * `offset` - The character offset for which to find the line and column.
///
/// # Returns
///
/// A tuple containing the zero-based line and column numbers.
fn get_line_col(input: &str, offset: usize) -> (usize, usize) {
let mut line = 0;
let mut col = 0;
for (i, ch) in input.chars().enumerate() {
if i == offset {
break;
}
if ch == '\n' {
line += 1;
col = 0;
} else {
col += 1;
}
}
(line, col)
}
/// Generates a window of text surrounding the error location for context.
/// The window is centered around the error column, with a maximum width of 40 characters.
///
/// # Arguments
///
/// * `input` - The original input string.
/// * `error_line` - The zero-based line number of the error.
/// * `error_col` - The zero-based column number of the error.
///
/// # Returns
///
/// A tuple containing a string with the window of text and the offset for the error pointer.
fn generate_window(input: &str, error_line: usize, error_col: usize) -> (String, usize) {
let window_width = 40;
let lines: Vec<&str> = input.lines().collect();
if lines.len() <= error_line {
return ("Error line out of range".into(), 0);
}
let offending_line = lines[error_line];
let line_len = offending_line.len();
let start = if error_col > 10 {
error_col.saturating_sub(10)
} else {
0
};
let end = (start + window_width).min(line_len);
(offending_line[start..end].to_string(), error_col - start)
}
}
impl fmt::Display for ShellErrorFull {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.error {
ShellError::IoError(err) => {
writeln!(f, "I/O Error: {}", err)?;
self.format_error_context(None);
}
ShellError::ExecFailed(msg, code, span) => {
writeln!(
f,
"Execution failed (exit code {}): {}",
code,
msg
)?;
self.format_error_context(Some(*span));
}
ShellError::ParsingError(msg, span) => {
writeln!(f, "Parsing error: {}", msg)?;
self.format_error_context(Some(*span));
}
ShellError::InvalidSyntax(msg, span) => {
writeln!(f, "Syntax Error: {}", msg)?;
self.format_error_context(Some(*span));
}
ShellError::CommandNotFound(msg, span) => {
writeln!(f, "Command not found: {}", msg)?;
self.format_error_context(Some(*span));
}
ShellError::InternalError(msg) => {
writeln!(f, "Internal Error: {}", msg)?;
self.format_error_context(None);
}
}
writeln!(f)?;
Ok(())
}
}
#[derive(Debug,PartialEq)]
pub enum ShellEvent {
Prompt,
Signal(Signals),
NewAST(Node),
CatchError(ShellError),
Exit(i32)
}
#[derive(Debug,PartialEq)]
pub enum Signals {
SIGINT,
SIGIO,
SIGPIPE,
SIGTSTP,
SIGQUIT,
SIGTERM,
SIGCHLD,
SIGHUP,
SIGWINCH,
SIGUSR1,
SIGUSR2
}
pub struct EventLoop<'a> {
sender: mpsc::Sender<ShellEvent>,
receiver: mpsc::Receiver<ShellEvent>,
shellenv: &'a mut ShellEnv
}
impl<'a> EventLoop<'a> {
/// Creates a new `EventLoop` instance with a message passing channel.
///
/// # Returns
/// A new instance of `EventLoop` with a sender and receiver for inter-task communication.
pub fn new(shellenv: &'a mut ShellEnv) -> Self {
let (sender, receiver) = mpsc::channel(100);
Self {
sender,
receiver,
shellenv
}
}
/// Provides a clone of the `sender` channel to send events to the event loop.
///
/// # Returns
/// A clone of the `mpsc::Sender<ShellEvent>` for sending events.
pub fn inbox(&self) -> mpsc::Sender<ShellEvent> {
self.sender.clone()
}
/// Starts the event loop and listens for incoming events.
///
/// This method spawns a separate task to listen for system signals using a `SignalListener`,
/// and then begins processing events from the channel.
///
/// # Returns
/// A `Result` containing the exit code (`i32`) or a `ShellError` if an error occurs.
pub async fn listen(&mut self) -> Result<i32, ShellError> {
let mut signal_listener = SignalListener::new(self.inbox());
tokio::spawn(async move {
signal_listener.signal_listen().await
});
self.event_listen().await
}
/// Processes events from the event loop's receiver channel.
///
/// This method handles different types of `ShellEvent` messages, such as prompting the user,
/// handling exit signals, processing new AST nodes, and responding to subprocess exits or errors.
///
/// # Returns
/// A `Result` containing the exit code (`i32`) or a `ShellError` if an error occurs.
pub async fn event_listen(&mut self) -> Result<i32, ShellError> {
debug!("Event loop started.");
let mut code: i32 = 0;
// Send an initial prompt event to the loop.
self.sender.send(ShellEvent::Prompt).await.unwrap();
while let Some(event) = self.receiver.recv().await {
match event {
ShellEvent::Prompt => {
let inbox = self.inbox();
let mut shellenv = self.shellenv.clone();
// Trigger the prompt logic.
tokio::spawn(async move {
prompt::prompt(inbox,&mut shellenv).await;
});
}
ShellEvent::Exit(exit_code) => {
// Handle exit events and set the exit code.
code = exit_code;
}
ShellEvent::NewAST(tree) => {
// Log and process a new AST node.
debug!("new tree:\n {:#?}", tree);
let mut walker = execute::NodeWalker::new(tree,self.shellenv);
match walker.start_walk() {
Ok(code) => {
info!("Last exit status: {:?}",code);
if let RshWaitStatus::Fail { code, cmd, span } = &code {
if *code == 127 {
if let Some(cmd) = cmd {
let err = ShellErrorFull::from(self.shellenv.get_last_input(),ShellError::from_no_cmd(cmd, *span));
eprintln!("{}",err);
}
};
};
self.shellenv.handle_exit_status(code);
},
Err(e) => {
let err = ShellErrorFull::from(self.shellenv.get_last_input(),e);
eprintln!("{}",err);
}
}
}
ShellEvent::Signal(signal) => {
// Handle received signals.
if let Err(e) = self.handle_signal(signal).await {
eprintln!("{}",ShellErrorFull::from(self.shellenv.get_last_input(), e));
}
}
ShellEvent::CatchError(err) => {
// Handle errors, exiting if fatal.
let fatal = err.is_fatal();
let error_display = ShellErrorFull::from(self.shellenv.get_last_input(), err);
if fatal {
eprintln!("Fatal: {}", error_display);
std::process::exit(1);
} else {
println!("{}",error_display);
}
}
}
}
Ok(code)
}
async fn handle_signal(&mut self, signal: Signals) -> Result<(), ShellError> {
match signal {
Signals::SIGQUIT => {
std::process::exit(0);
}
Signals::SIGCHLD => {
let job_pid: Pid;
let status = match waitpid(None, Some(WaitPidFlag::WNOHANG)) {
Ok(WaitStatus::Exited(pid,status)) => {
job_pid = pid;
if status == 0 {
RshWaitStatus::Success { span: Span::new() }
} else {
RshWaitStatus::Fail { code: status, cmd: None, span: Span::new() }
}
}
Ok(WaitStatus::StillAlive) => return Ok(()),
Ok(WaitStatus::Signaled(pid, sig, _)) => {
job_pid = pid;
RshWaitStatus::Signaled { sig }
}
Ok(WaitStatus::Stopped(pid, sig)) => {
job_pid = pid;
RshWaitStatus::Stopped { sig }
}
Err(_) => { /* No child processes found, so */ return Ok(()) },
_ => unimplemented!()
};
let jobs = self.shellenv.borrow_jobs();
for job in jobs {
let job = job.1;
if *job.pgid() == job_pid {
println!();
job.status(status);
println!("{}",job);
break
}
}
}
_ => { },
}
Ok(())
}
}
pub struct SignalListener {
outbox: mpsc::Sender<ShellEvent>,
sigint: Signal,
sigio: Signal,
sigpipe: Signal,
sigtstp: Signal,
sigquit: Signal,
sigterm: Signal,
sigchild: Signal,
sighup: Signal,
sigwinch: Signal,
sigusr1: Signal,
sigusr2: Signal,
}
impl SignalListener {
pub fn new(outbox: mpsc::Sender<ShellEvent>) -> Self {
Self {
// Signal listeners
// TODO: figure out what to do instead of unwrapping
outbox,
sigint: signal(SignalKind::interrupt()).unwrap(),
sigio: signal(SignalKind::io()).unwrap(),
sigpipe: signal(SignalKind::pipe()).unwrap(),
sigtstp: signal(SignalKind::from_raw(20)).unwrap(),
sigquit: signal(SignalKind::quit()).unwrap(),
sigterm: signal(SignalKind::terminate()).unwrap(),
sigchild: signal(SignalKind::child()).unwrap(),
sighup: signal(SignalKind::hangup()).unwrap(),
sigwinch: signal(SignalKind::window_change()).unwrap(),
sigusr1: signal(SignalKind::user_defined1()).unwrap(),
sigusr2: signal(SignalKind::user_defined2()).unwrap(),
}
}
pub async fn signal_listen(&mut self) -> Result<i32, ShellError> {
let sigint = &mut self.sigint;
let sigio = &mut self.sigio;
let sigpipe = &mut self.sigpipe;
let sigtstp = &mut self.sigtstp;
let sigquit = &mut self.sigquit;
let sigterm = &mut self.sigterm;
let sigchild = &mut self.sigchild;
let sighup = &mut self.sighup;
let sigwinch = &mut self.sigwinch;
let sigusr1 = &mut self.sigusr1;
let sigusr2 = &mut self.sigusr2;
loop {
tokio::select! {
_ = sigint.recv() => {
self.outbox.send(ShellEvent::Signal(Signals::SIGINT)).await.unwrap();
// Handle SIGINT
}
_ = sigio.recv() => {
self.outbox.send(ShellEvent::Signal(Signals::SIGIO)).await.unwrap();
// Handle SIGIO
}
_ = sigpipe.recv() => {
self.outbox.send(ShellEvent::Signal(Signals::SIGPIPE)).await.unwrap();
// Handle SIGPIPE
}
_ = sigtstp.recv() => {
self.outbox.send(ShellEvent::Signal(Signals::SIGTSTP)).await.unwrap();
// Handle SIGPIPE
}
_ = sigquit.recv() => {
self.outbox.send(ShellEvent::Signal(Signals::SIGQUIT)).await.unwrap();
// Handle SIGQUIT
}
_ = sigterm.recv() => {
self.outbox.send(ShellEvent::Signal(Signals::SIGTERM)).await.unwrap();
// Handle SIGTERM
}
_ = sigchild.recv() => {
self.outbox.send(ShellEvent::Signal(Signals::SIGCHLD)).await.unwrap();
// Handle SIGCHLD
}
_ = sighup.recv() => {
self.outbox.send(ShellEvent::Signal(Signals::SIGHUP)).await.unwrap();
// Handle SIGHUP
}
_ = sigwinch.recv() => {
self.outbox.send(ShellEvent::Signal(Signals::SIGWINCH)).await.unwrap();
// Handle SIGWINCH
}
_ = sigusr1.recv() => {
self.outbox.send(ShellEvent::Signal(Signals::SIGUSR1)).await.unwrap();
// Handle SIGUSR1
}
_ = sigusr2.recv() => {
self.outbox.send(ShellEvent::Signal(Signals::SIGUSR2)).await.unwrap();
// Handle SIGUSR2
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
[?2004h
projects/rust/rsh/src/
$>


projects/rust/rsh/src/
$> export PS1=">> "

View File

@@ -0,0 +1,2 @@
#!/usr/bin/env bash
echo hellothere$@

View File

@@ -0,0 +1,3 @@
PROCESSED: DATA 0
PROCESSED: DATA 1
PROCESSED: DATA 2

View File

@@ -0,0 +1,125 @@
use std::fmt;
use crate::interp;
use interp::parse::{NdType,Node};
impl fmt::Display for Node {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn write_tree(node: &Node, f: &mut fmt::Formatter<'_>, indent: usize) -> fmt::Result {
let prefix = "| ".repeat(indent);
writeln!(f, "{}Node (span: {:?})", prefix, node.span)?;
match &node.nd_type {
NdType::Root { deck } => {
writeln!(f, "{}Root", prefix)?;
for node in deck {
write_tree(node, f, indent + 1)?;
}
}
NdType::If { cond_blocks, else_block } => {
writeln!(f, "{}If", prefix)?;
for block in cond_blocks {
writeln!(f, "{}|--- Condition", prefix)?;
write_tree(&block.condition, f, indent + 2)?;
writeln!(f, "{}|--- Body", prefix)?;
write_tree(&block.body, f, indent + 2)?;
}
if let Some(else_block) = else_block {
writeln!(f, "{}|--- Else", prefix)?;
write_tree(else_block, f, indent + 1)?;
}
}
NdType::For { loop_vars, loop_arr, loop_body } => {
let var_texts: Vec<_> = loop_vars.iter().map(|var| var.text()).collect();
let arr_texts: Vec<_> = loop_arr.iter().map(|arr| arr.text()).collect();
writeln!(f, "{}For", prefix)?;
writeln!(f, "{}|--- Loop Vars: {:?}", prefix, var_texts)?;
writeln!(f, "{}|--- Loop Array: {:?}", prefix, arr_texts)?;
writeln!(f, "{}|--- Body", prefix)?;
write_tree(loop_body, f, indent + 1)?;
}
NdType::Loop { condition, logic } => {
writeln!(f, "{}Loop (condition: {})", prefix, condition)?;
writeln!(f, "{}|--- Condition", prefix)?;
write_tree(&logic.condition, f, indent + 1)?;
writeln!(f, "{}|--- Body", prefix)?;
write_tree(&logic.body, f, indent + 1)?;
}
NdType::Case { input_var, cases } => {
writeln!(f, "{}Case (input: {:?})", prefix, input_var.text())?;
for case in cases {
writeln!(f, "{}|--- Pattern: {:?}", prefix, case.0)?;
write_tree(case.1, f, indent + 1)?;
}
}
NdType::Select { select_var, opts, body } => {
let opts_texts: Vec<_> = opts.iter().map(|opt| opt.text()).collect();
writeln!(
f,
"{}Select (input: {:?}, options: {:?})",
prefix, select_var.text(), opts_texts
)?;
write_tree(body, f, indent + 1)?;
}
NdType::Pipeline { left, right, both: _ } => {
writeln!(f, "{}Pipeline", prefix)?;
writeln!(f, "{}|--- Left", prefix)?;
write_tree(left, f, indent + 1)?;
writeln!(f, "{}|--- Right", prefix)?;
write_tree(right, f, indent + 1)?;
}
NdType::Chain { left, right, op } => {
writeln!(f, "{}Chain (operator span: {:?})", prefix, op.span)?;
writeln!(f, "{}|--- Left", prefix)?;
write_tree(left, f, indent + 1)?;
writeln!(f, "{}|--- Right", prefix)?;
write_tree(right, f, indent + 1)?;
}
NdType::BraceGroup { body } => {
writeln!(f, "{}BraceGroup", prefix)?;
write_tree(body, f, indent + 1)?;
}
NdType::Subshell { body, argv: _ } => {
writeln!(f, "{}Subshell (body: {:?})", prefix, body)?;
}
NdType::FuncDef { name, body } => {
writeln!(f, "{}Function Definition (name: {})", prefix, name)?;
write_tree(body, f, indent + 1)?;
}
NdType::Assignment { name, value } => {
writeln!(f, "{}Assignment (name: {}, value: {:?})", prefix, name, value)?;
}
NdType::Command { argv } | NdType::Builtin { argv } => {
let argv_texts: Vec<_> = argv.iter().map(|arg| arg.text()).collect();
writeln!(
f,
"{}Command (args: {:?})",
prefix, argv_texts
)?;
}
NdType::And => {
writeln!(f, "{}And", prefix)?;
}
NdType::Or => {
writeln!(f, "{}Or", prefix)?;
}
NdType::Redirection { .. } => {
writeln!(f, "{}Redir", prefix)?;
}
NdType::Pipe => {
writeln!(f, "{}Pipe", prefix)?;
}
NdType::PipeBoth => {
writeln!(f, "{}PipeBoth", prefix)?;
}
NdType::Cmdsep => {
writeln!(f, "{}Cmdsep", prefix)?;
}
NdType::NullNode => panic!()
}
Ok(())
}
write_tree(self, f, 0)
}
}

View File

@@ -0,0 +1,639 @@
use chrono::Local;
use glob::glob;
use log::{debug, info, trace};
use std::collections::VecDeque;
use std::mem::take;
use std::path::PathBuf;
use crate::event::ShellError;
use crate::interp::token::{Tk, TkType, WdFlags, WordDesc};
use crate::interp::helper::{self,StrExtension, VecDequeExtension};
use crate::shellenv::{EnvFlags, ShellEnv};
use super::parse::{self, NdType, Node, ParseState};
use super::token::RshTokenizer;
pub fn check_globs(string: String) -> bool {
string.has_unescaped("?") ||
string.has_unescaped("*")
}
pub fn expand_arguments(shellenv: &ShellEnv, node: &mut Node) -> Result<Vec<Tk>,ShellError> {
let argv = node.get_argv()?;
let mut expand_buffer = Vec::new();
for arg in &argv {
let mut expanded = expand_token(shellenv, arg.clone());
while let Some(token) = expanded.pop_front() {
if !token.text().is_empty() {
// Do not return empty tokens
expand_buffer.push(token);
}
}
}
match &node.nd_type {
NdType::Builtin {..} => {
node.nd_type = NdType::Builtin { argv: expand_buffer.clone().into() };
Ok(expand_buffer)
}
NdType::Command {..} => {
node.nd_type = NdType::Command { argv: expand_buffer.clone().into() };
Ok(expand_buffer)
}
NdType::Subshell { body, argv: _ } => {
node.nd_type = NdType::Subshell { body: body.to_string(), argv: expand_buffer.clone().into() };
Ok(expand_buffer)
}
_ => Err(ShellError::from_internal("Called expand arguments on a non-command node"))
}
}
pub fn esc_seq(c: char) -> Option<char> {
//TODO:
match c {
'a' => Some('\x07'),
'n' => Some('\n'),
't' => Some('\t'),
'\\' => Some('\\'),
'"' => Some('"'),
'\'' => Some('\''),
_ => panic!()
}
}
pub fn expand_time(fmt: &str) -> String {
let right_here_right_now = Local::now();
right_here_right_now.format(fmt).to_string()
}
pub fn expand_prompt(shellenv: &ShellEnv) -> String {
// TODO:
//\j - number of managed jobs
//\l - shell's terminal device name
//\v - rsh version
//\V - rsh release; version + patch level
//\! - history number of this command
//\# - command number of this command
let default_color = if shellenv.env_vars.get("UID").is_some_and(|uid| uid == "0") {
"31" // Red if uid is 0, aka root user
} else {
"32" // Green if anyone else
};
let cwd: String = shellenv.env_vars.get("PWD").map_or("", |cwd| cwd).to_string();
let default_path = if cwd.as_str() == "/" {
"\\e[36m\\w\\e[0m".to_string()
} else {
format!("\\e[1;{}m\\w\\e[1;36m/\\e[0m",default_color)
};
let ps1: String = shellenv.env_vars.get("PS1").map_or(format!("\\n{}\\n\\e[{}mdebug \\$\\e[36m>\\e[0m ",default_path,default_color), |ps1| ps1.clone());
let mut result = String::new();
let mut chars = ps1.chars().collect::<VecDeque<char>>();
while let Some(c) = chars.pop_front() {
match c {
'\\' => {
if let Some(esc_c) = chars.pop_front() {
match esc_c {
'a' => result.push('\x07'),
'n' => result.push('\n'),
'r' => result.push('\r'),
'\\' => result.push('\\'),
'\'' => result.push('\''),
'"' => result.push('"'),
'd' => result.push_str(expand_time("%a %b %d").as_str()),
't' => result.push_str(expand_time("%H:%M:%S").as_str()),
'T' => result.push_str(expand_time("%I:%M:%S").as_str()),
'A' => result.push_str(expand_time("%H:%M").as_str()),
'@' => result.push_str(expand_time("%I:%M %p").as_str()),
_ if esc_c.is_digit(8) => {
let mut octal_digits = String::new();
octal_digits.push(esc_c); // Add the first digit
for _ in 0..2 {
if let Some(next_c) = chars.front() {
if next_c.is_digit(8) {
octal_digits.push(chars.pop_front().unwrap());
} else {
break;
}
}
}
if let Ok(value) = u8::from_str_radix(&octal_digits, 8) {
result.push(value as char);
} else {
// Invalid sequence, treat as literal
result.push_str(&format!("\\{}", octal_digits));
}
}
'e' => {
result.push('\x1B');
if chars.front().is_some_and(|&ch| ch == '[') {
result.push(chars.pop_front().unwrap()); // Consume '['
while let Some(ch) = chars.pop_front() {
result.push(ch);
if ch == 'm' {
break; // End of ANSI sequence
}
}
}
}
'[' => {
// Handle \[ (start of non-printing sequence)
while let Some(ch) = chars.pop_front() {
if ch == ']' {
break; // Stop at the closing \]
}
result.push(ch); // Add non-printing content
}
}
']' => {
// Handle \] (end of non-printing sequence)
// Do nothing, it's just a marker
}
'w' => {
let mut cwd = shellenv.env_vars.get("PWD").map_or(String::new(), |pwd| pwd.to_string());
let home = shellenv.env_vars.get("HOME").map_or("", |home| home);
if cwd.starts_with(home) {
cwd = cwd.replacen(home, "~", 1); // Use `replacen` to replace only the first occurrence
}
// TODO: unwrap is probably safe here since this option is initialized with the environment but it might still cause issues later if this is left unhandled
let trunc_len = shellenv.shopts.get("trunc_prompt_path").unwrap_or(&0);
if *trunc_len > 0 {
let mut path = PathBuf::from(cwd);
let mut cwd_components: Vec<_> = path.components().collect();
if cwd_components.len() > *trunc_len {
cwd_components = cwd_components.split_off(cwd_components.len() - *trunc_len);
path = cwd_components.iter().collect(); // Rebuild the PathBuf
}
cwd = path.to_string_lossy().to_string();
}
result.push_str(&cwd);
}
'W' => {
let cwd = PathBuf::from(shellenv.env_vars.get("PWD").map_or("", |pwd| pwd));
let mut cwd = cwd.components().last().map(|comp| comp.as_os_str().to_string_lossy().to_string()).unwrap_or_default();
let home = shellenv.env_vars.get("HOME").map_or("", |home| home);
if cwd.starts_with(home) {
cwd = cwd.replacen(home, "~", 1); // Replace HOME with '~'
}
result.push_str(&cwd);
}
'H' => {
let hostname = shellenv.env_vars.get("HOSTNAME").map_or("unknown host", |host| host);
result.push_str(hostname);
}
'h' => {
let hostname = shellenv.env_vars.get("HOSTNAME").map_or("unknown host", |host| host);
if let Some((hostname, _)) = hostname.split_once('.') {
result.push_str(hostname);
} else {
result.push_str(hostname); // No '.' found, use the full hostname
}
}
's' => {
let sh_name = shellenv.env_vars.get("SHELL").map_or("rsh", |sh| sh);
result.push_str(sh_name);
}
'u' => {
let user = shellenv.env_vars.get("USER").map_or("unknown", |user| user);
result.push_str(user);
}
'$' => {
let uid = shellenv.env_vars.get("UID").map_or("0", |uid| uid);
match uid {
"0" => result.push('#'),
_ => result.push('$'),
}
}
_ => {
result.push('\\');
result.push(esc_c);
}
}
} else {
result.push('\\');
}
}
_ => result.push(c)
}
}
result
}
pub fn process_ansi_escapes(input: &str) -> String {
let mut result = String::new();
let mut chars = input.chars().collect::<VecDeque<char>>();
while let Some(c) = chars.pop_front() {
if c == '\\' {
if let Some(next) = chars.pop_front() {
match next {
'a' => result.push('\x07'), // Bell
'b' => result.push('\x08'), // Backspace
't' => result.push('\t'), // Tab
'n' => result.push('\n'), // Newline
'r' => result.push('\r'), // Carriage return
'e' | 'E' => result.push('\x1B'), // Escape (\033 in octal)
'0' => {
// Octal escape: \0 followed by up to 3 octal digits
let mut octal_digits = String::new();
while octal_digits.len() < 3 && chars.front().is_some_and(|ch| ch.is_digit(8)) {
octal_digits.push(chars.pop_front().unwrap());
}
if let Ok(value) = u8::from_str_radix(&octal_digits, 8) {
let character = value as char;
result.push(character);
// Check for ANSI sequence if the result is ESC (\033 or \x1B)
if character == '\x1B' && chars.front().is_some_and(|&ch| ch == '[') {
result.push(chars.pop_front().unwrap()); // Consume '['
while let Some(ch) = chars.pop_front() {
result.push(ch);
if ch == 'm' {
break; // Stop at the end of the ANSI sequence
}
}
}
}
}
_ => {
// Unknown escape, treat literally
result.push('\\');
result.push(next);
}
}
} else {
// Trailing backslash, treat literally
result.push('\\');
}
} else if c == '\x1B' {
// Handle raw ESC characters (e.g., \033 in octal or actual ESC char)
result.push(c);
if chars.front().is_some_and(|&ch| ch == '[') {
result.push(chars.pop_front().unwrap()); // Consume '['
while let Some(ch) = chars.pop_front() {
result.push(ch);
if ch == 'm' {
break; // Stop at the end of the ANSI sequence
}
}
}
} else {
result.push(c);
}
}
result
}
pub fn expand_alias(shellenv: &ShellEnv, mut node: Node) -> Result<Node, ShellError> {
match node.nd_type {
NdType::Command { ref mut argv } | NdType::Builtin { ref mut argv } => {
if let Some(cmd_tk) = argv.pop_front() {
if let Some(alias_content) = shellenv.get_alias(cmd_tk.text()) {
let new_argv = take(argv);
let mut child_shellenv = shellenv.clone();
child_shellenv.mod_flags(|f| *f |= EnvFlags::NO_ALIAS);
let mut state = ParseState {
input: alias_content,
shellenv: &child_shellenv,
tokens: VecDeque::new(),
ast: Node::new(),
};
let mut tokenizer = RshTokenizer::new(alias_content);
tokenizer.tokenize();
state.tokens = tokenizer.tokens.into();
let mut alias_tokens = state.tokens;
// Trim `SOI` and `EOI` tokens
alias_tokens.pop_back();
alias_tokens.pop_front();
alias_tokens.extend(new_argv);
// Guard against recursive aliasing
// i.e. "alias grep="grep --color-auto"
if alias_tokens.front().is_some_and(|tk| tk.text() == cmd_tk.text()) {
for token in &mut alias_tokens {
if token.text() == cmd_tk.text() {
token.wd.flags |= WdFlags::FROM_ALIAS
}
}
}
state.tokens = alias_tokens;
state = parse::parse(state)?;
for redir in node.redirs {
state.ast.redirs.push_back(redir.clone())
}
Ok(state.ast)
} else {
argv.push_front(cmd_tk);
Ok(node)
}
} else {
Ok(node)
}
}
_ => unreachable!()
}
}
pub fn expand_token(shellenv: &ShellEnv, token: Tk) -> VecDeque<Tk> {
trace!("expand(): Starting expansion with token: {:?}", token);
let mut working_buffer: VecDeque<Tk> = VecDeque::new();
let mut product_buffer: VecDeque<Tk> = VecDeque::new();
//TODO: find some way to clean up this surprisingly functional mess
// Escaping breaks this right now I think
working_buffer.push_back(token.clone());
while let Some(mut token) = working_buffer.pop_front() {
let is_glob = check_globs(token.text().into());
let is_brace_expansion = helper::is_brace_expansion(token.text());
let expand_home = token.text().has_unescaped("~");
if expand_home {
// If this unwrap fails, god help you
let home = shellenv.get_variable("HOME").unwrap();
token.wd.text = token.wd.text.replace("~",&home);
}
if !is_glob && !is_brace_expansion {
debug!("expanding var for {}",token.text());
if token.text().has_unescaped("$") && !token.wd.flags.intersects(WdFlags::FROM_VAR | WdFlags::SNG_QUOTED) {
info!("found unescaped dollar in: {}",token.text());
if token.text().has_unescaped("$@") {
let mut param_tokens = expand_params(shellenv,token);
while let Some(param) = param_tokens.pop_back() {
working_buffer.push_front(param);
}
continue
}
token.wd.text = expand_var(shellenv, token.text().into());
}
if helper::is_brace_expansion(token.text()) || token.text().has_unescaped("$") {
working_buffer.push_front(token);
} else {
let expand_home = token.text().has_unescaped("~");
if expand_home {
// If this unwrap fails, god help you
let home = shellenv.get_variable("HOME").unwrap();
token.wd.text = token.wd.text.replace("~",&home);
}
product_buffer.push_back(token)
}
} else if is_brace_expansion && token.text().has_unescaped("{") && token.tk_type != TkType::String {
// Perform brace expansion
let expanded = expand_braces(token.text().to_string(), VecDeque::new());
for mut expanded_token in expanded {
expanded_token = expand_var(shellenv, expanded_token);
working_buffer.push_back(
Tk {
tk_type: TkType::Expanded,
wd: WordDesc {
text: expanded_token,
span: token.span(),
flags: token.flags()
}
}
);
};
} else if is_glob {
// Expand glob patterns
for path in glob(token.text()).unwrap().flatten() {
working_buffer.push_back(
Tk {
tk_type: TkType::Expanded,
wd: WordDesc {
text: path.to_str().unwrap().to_string(),
span: token.span(),
flags: token.flags()
}
}
);
}
} else if shellenv.get_alias(token.text()).is_some() {
let alias_content = shellenv.get_alias(token.text()).unwrap().split(' ');
for word in alias_content {
working_buffer.push_back(
Tk {
tk_type: TkType::Expanded,
wd: WordDesc {
text: word.into(),
span: token.span(),
flags: token.flags()
}
}
);
}
} else {
let expand_home = token.text().has_unescaped("~");
if expand_home {
// If this unwrap fails, god help you
let home = shellenv.get_variable("HOME").unwrap();
token.wd.text = token.wd.text.replace("~",&home);
}
product_buffer.push_back(token);
}
}
let mut temp_buffer = VecDeque::new();
product_buffer.map_rotate(|mut elem| {
elem.wd.text = elem.wd.text.consume_escapes();
temp_buffer.push_back(elem);
});
product_buffer.extend(temp_buffer.drain(..));
product_buffer
}
pub fn clean_escape_chars(token_buffer: &mut VecDeque<Tk>) {
for token in token_buffer {
let mut text = std::mem::take(&mut token.wd.text);
text = text.replace('\\',"");
token.wd.text = text;
}
}
pub fn expand_braces(word: String, mut results: VecDeque<String>) -> VecDeque<String> {
if let Some((preamble, rest)) = word.split_once("{") {
if let Some((amble, postamble)) = rest.split_last("}") {
// the current logic makes adjacent expansions look like this: `left}{right`
// let's take advantage of that, shall we
if let Some((left,right)) = amble.split_once("}{") {
// Reconstruct the left side into a new brace expansion: left -> {left}
let left = format!("{}{}{}","{",left,"}");
// Same with the right side: right -> {right}
// This also has the side effect of rebuilding any subsequent adjacent expansions
// e.g. "right1}{right2}{right3" -> {right1}{right2}{right3}
let right = format!("{}{}{}","{",right,"}");
// Recurse
let left_expanded = expand_braces(left.to_string(), VecDeque::new());
let right_expanded = expand_braces(right.to_string(), VecDeque::new());
// Combine them
for left_part in left_expanded {
for right_part in &right_expanded {
results.push_back(format!("{}{}",left_part,right_part));
}
}
} else {
let mut expanded = expand_amble(amble);
while let Some(string) = expanded.pop_front() {
let expanded_word = format!("{}{}{}", preamble, string, postamble);
results = expand_braces(expanded_word, results); // Recurse for nested braces
}
}
} else {
// Malformed brace: No closing `}` found
results.push_back(word);
}
} else {
// Base case: No more braces to expand
results.push_back(word);
}
results
}
pub fn expand_amble(amble: String) -> VecDeque<String> {
let mut result = VecDeque::new();
if amble.contains("..") && amble.len() >= 4 {
let num_range = amble.chars().next().is_some_and(|ch| ch.is_ascii_digit())
&& amble.chars().last().is_some_and(|ch| ch.is_ascii_digit());
let lower_alpha_range = amble.chars().next().is_some_and(|ch| ch.is_ascii_lowercase())
&& amble.chars().last().is_some_and(|ch| ch.is_ascii_lowercase())
&& amble.chars().next() <= amble.chars().last(); // Ensure valid range
let upper_alpha_range = amble.chars().next().is_some_and(|ch| ch.is_ascii_uppercase())
&& amble.chars().last().is_some_and(|ch| ch.is_ascii_uppercase())
&& amble.chars().next() <= amble.chars().last(); // Ensure valid range
if lower_alpha_range || upper_alpha_range {
let left = amble.chars().next().unwrap();
let right = amble.chars().last().unwrap();
for i in left..=right {
result.push_back(i.to_string());
}
}
if num_range {
let (left,right) = amble.split_once("..").unwrap();
for i in left.parse::<i32>().unwrap()..=right.parse::<i32>().unwrap() {
result.push_back(i.to_string());
}
}
} else {
let mut cur_string = String::new();
let mut chars = amble.chars();
let mut brace_stack = vec![];
while let Some(ch) = chars.next() {
match ch {
'{' => {
cur_string.push(ch);
brace_stack.push(ch);
}
'}' => {
cur_string.push(ch);
brace_stack.pop();
}
',' => {
if brace_stack.is_empty() {
result.push_back(cur_string);
cur_string = String::new();
} else {
cur_string.push(ch)
}
}
'\\' => {
let next = chars.next();
if !matches!(next, Some('}') | Some('{')) {
cur_string.push(ch)
}
if let Some(next) = next {
cur_string.push(next)
}
}
_ => cur_string.push(ch)
}
}
result.push_back(cur_string);
}
result
}
pub fn expand_var(shellenv: &ShellEnv, string: String) -> String {
let mut left = String::new();
let mut right = String::new();
let mut chars = string.chars().collect::<VecDeque<char>>();
while let Some(ch) = chars.pop_front() {
match ch {
'\\' => {
left.push(ch);
left.push(if let Some(ch) = chars.pop_front() { ch } else { break })
}
'$' => {
right.extend(chars.drain(..));
break
},
_ => left.push(ch)
}
}
if right.is_empty() {
return string.to_string()
}
let mut right_chars = right.chars().collect::<VecDeque<char>>();
let mut var_name = String::new();
while let Some(ch) = right_chars.pop_front() {
match ch {
_ if ch.is_alphanumeric() => {
var_name.push(ch);
}
'_' => {
var_name.push(ch);
}
'-' | '*' | '?' | '$' | '#' => {
var_name.push(ch);
break
}
'{' => continue,
'}' => break,
_ => {
right_chars.push_front(ch);
break
}
}
}
let right = right_chars.iter().collect::<String>();
let value = shellenv.get_variable(&var_name).unwrap_or_default();
let expanded = format!("{}{}{}",left,value,right);
if expanded.has_unescaped("$") {
expand_var(shellenv,expanded)
} else {
expanded
}
}
fn expand_params(shellenv: &ShellEnv, token: Tk) -> VecDeque<Tk> {
let mut expanded_tokens = VecDeque::new();
// Get the positional parameter string from shellenv and split it
let arg_string = shellenv.get_variable("@").unwrap_or_default();
let arg_split = arg_string.split(' ');
// Split the token's text at the first instance of '$@' and make two new tokens
// Subsequent instances will be handled in later iterations of expand()
let (left,right) = token.text().split_once("$@").unwrap();
let left_token = Tk::new(left.to_string(), token.span(), token.flags());
let right_token = Tk::new(right.to_string(), token.span(), token.flags());
// Push the left token into the deque
if !left_token.text().is_empty() {
expanded_tokens.push_back(left_token);
}
for arg in arg_split {
// For each arg, make a new token and push it into the deque
let new_token = Tk::new(arg.to_string(),token.span(), token.flags() | WdFlags::FROM_VAR);
expanded_tokens.push_back(new_token);
}
// Now push the right token into the deque
if !right_token.text().is_empty() {
expanded_tokens.push_back(right_token);
}
expanded_tokens
}

View File

@@ -0,0 +1,252 @@
use crate::{interp::token::REGEX, shellenv::ShellEnv};
use nix::unistd::dup2;
use std::{collections::VecDeque, env, fs::{self, metadata}, io, os::{fd::AsRawFd, unix::fs::PermissionsExt}, path::Path};
use super::parse::{NdType, Node};
pub trait VecExtension<T> {
fn extended(self, vec: Vec<T>) -> Vec<T>;
}
impl<T> VecExtension<T> for Vec<T> {
fn extended(self, vec: Vec<T>) -> Vec<T> {
let mut new_vec = self;
new_vec.extend(vec);
new_vec
}
}
pub trait VecDequeExtension<T> {
fn map_rotate<F>(&mut self, transform: F) where F: FnMut(T);
}
impl<T> VecDequeExtension<T> for VecDeque<T> {
/// Applies a transformation function to each element of the `VecDeque`
/// while preserving the original order of elements.
///
/// This method "rotates" the `VecDeque` by repeatedly removing the element
/// at the front, applying the given transformation function to it, and
/// appending it to the back. The process ensures that all elements are
/// visited exactly once, and the final order remains unchanged.
///
/// # Type Parameters
/// - `T`: The type of elements in the `VecDeque`.
/// - `F`: A closure or function that takes a mutable reference to an element
/// and applies the desired transformation.
///
/// # Parameters
/// - `transform`: A closure or function of type `FnMut(&mut T)` that modifies
/// each element in place.
///
/// # Examples
///
/// ```rust
/// use std::collections::VecDeque;
///
/// trait VecDequeExtension<T> {
/// fn map_rotate<F>(&mut self, transform: F)
/// where
/// F: FnMut(&mut T);
/// }
///
/// impl<T> VecDequeExtension<T> for VecDeque<T> {
/// fn map_rotate<F>(&mut self, mut transform: F)
/// where
/// F: FnMut(&mut T),
/// {
/// let len = self.len();
/// for _ in 0..len {
/// if let Some(mut element) = self.pop_front() {
/// transform(&mut element);
/// self.push_back(element);
/// }
/// }
/// }
/// }
///
/// let mut deque: VecDeque<String> = VecDeque::from(vec![
/// String::from("hello"),
/// String::from("world"),
/// String::from("rust"),
/// ]);
///
/// // Capitalize all elements
/// deque.map_rotate(|text| *text = text.to_uppercase());
///
/// assert_eq!(deque, VecDeque::from(vec![
/// String::from("HELLO"),
/// String::from("WORLD"),
/// String::from("RUST"),
/// ]));
/// ```
fn map_rotate<F>(&mut self, mut transform: F)
where F: FnMut(T) {
let len = self.len();
for _ in 0..len {
if let Some(element) = self.pop_front() {
transform(element);
}
}
}
}
pub trait StrExtension {
fn split_last(&self, pat: &str) -> Option<(String,String)>;
fn has_unescaped(&self, pat: &str) -> bool;
fn consume_escapes(&self) -> String;
}
impl StrExtension for str {
fn consume_escapes(&self) -> String {
let mut result = String::new();
let mut chars = self.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\\' {
if let Some(&next_ch) = chars.peek() {
chars.next(); // Consume the escaped character
result.push(next_ch); // Add the unescaped pattern character
}
} else {
result.push(ch); // Add non-escaped characters as-is
}
}
result
}
fn split_last(&self, pat: &str) -> Option<(String, String)> {
let mut last_index = None;
let pat_len = pat.len();
// Iterate through the string and find the last occurrence of `pat`
for i in 0..=self.len().saturating_sub(pat_len) {
if &self[i..i + pat_len] == pat {
last_index = Some(i);
}
}
// If no occurrence is found, return None
last_index.map(|index| (
self[..index].to_string(), // Everything before `pat`
self[index + pat_len..].to_string(), // Everything after `pat`
))
}
/// Checks to see if a string slice contains a specified unescaped text pattern. This method assumes that '\' is the escape character.
///
fn has_unescaped(&self, pat: &str) -> bool {
let mut chars = self.chars().collect::<VecDeque<char>>();
let mut working_pat = String::new();
let mut escaped = false;
while let Some(ch) = chars.pop_front() {
if !escaped && working_pat.contains(pat) {
return true;
}
match ch {
' ' | '\t' => {
// Check for unescaped match when encountering a space/tab
// Reset for next segment
escaped = false;
working_pat.clear();
}
'\\' => {
escaped = true;
}
_ => {
working_pat.push(ch);
}
}
}
// Check for unescaped match at the end of the string
!escaped && working_pat.contains(pat)
}
}
// This is used when pesky system calls want to emit their own errors
// Which ends up being redundant, since rsh has it's own error reporting system
// Kind of hacky but fuck it
// It works by taking the function as an argument and then executing it in
// an isolated context where stderr is briefly redirected to /dev/null.
pub fn suppress_err<F: FnOnce() -> T, T>(f: F) -> T {
let stderr = io::stderr();
let stderr_fd = stderr.as_raw_fd();
let devnull = fs::OpenOptions::new().write(true).open("/dev/null").unwrap();
let devnull_fd = devnull.as_raw_fd();
dup2(devnull_fd, stderr_fd).unwrap();
let result = f();
dup2(stderr_fd,stderr_fd).unwrap();
result
}
pub fn which(shellenv: &ShellEnv, command: &str) -> Option<String> {
if let Some(env_path) = shellenv.get_variable("PATH") {
for path in env::split_paths(&env_path) {
let full_path = path.join(command);
if full_path.is_file() && is_exec(&full_path) {
return Some(full_path.to_string_lossy().to_string())
}
}
}
None
}
pub fn is_exec(path: &Path) -> bool {
fs::metadata(path)
.map(|meta| meta.is_file() && (meta.permissions().mode() & 0o111) != 0)
.unwrap_or(false)
}
pub fn flatten_pipeline(left: Node, right: Node, mut flattened: VecDeque<Node>) -> VecDeque<Node> {
flattened.push_front(right);
if let NdType::Pipeline { left, right, both: _ } = left.nd_type {
flattened = flatten_pipeline(*left, *right, flattened);
} else {
flattened.push_front(left);
}
flattened
}
pub fn is_brace_expansion(text: &str) -> bool {
if REGEX["brace_expansion"].is_match(text) &&
REGEX["brace_expansion"].captures(text).unwrap()[1].is_empty() {
let mut brace_count: i32 = 0;
let mut chars = text.chars();
while let Some(ch) = chars.next() {
match ch {
'{' => brace_count += 1,
'}' => {
brace_count -= 1;
if brace_count < 0 {
// found a closing brace before an open brace
return false
}
},
'\\' => { chars.next(); },
_ => { /* Do nothing */ }
}
}
brace_count == 0
} else {
false
}
}
#[cfg(test)]
mod test {
use super::StrExtension;
#[test]
fn split_last_test() {
let string = "hello there here is a pattern '&&&' and another occurence of it '&&&' and another! '&&&' a lot of patterns today";
if let Some((left,right)) = string.split_last("&&&") {
assert_eq!((left,right),("hello there here is a pattern '&&&' and another occurence of it '&&&' and another! '".into(),"' a lot of patterns today".into()))
} else {
panic!()
}
}
}

View File

@@ -0,0 +1,5 @@
pub mod parse;
pub mod helper;
pub mod token;
pub mod expand;
pub mod debug;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,985 @@
use bitflags::bitflags;
use once_cell::sync::Lazy;
use log::trace;
use regex::Regex;
use std::collections::HashMap;
use std::collections::VecDeque;
use std::io;
use std::mem::take;
use crate::interp::parse::Span;
use crate::event::ShellError;
use super::helper::StrExtension;
pub static REGEX: Lazy<HashMap<&'static str, Regex>> = Lazy::new(|| {
let mut regex = HashMap::new();
regex.insert("redirection",Regex::new(r"^(?P<fd_out>[0-9]+)?(?P<operator>>{1,2}|<{1,3})(?:[&]?(?P<fd_target>[0-9]+))?$").unwrap());
regex.insert("rsh_shebang",Regex::new(r"^#!((?:/[^\s]+)+)((?:\s+arg:[a-zA-Z][a-zA-Z0-9_\-]*)*)$").unwrap());
regex.insert("brace_expansion",Regex::new(r"(\$?)\{(?:[\x20-\x7e,]+|[0-9]+(?:\.\.[0-9+]){1,2}|[a-z]\.\.[a-z]|[A-Z]\.\.[A-Z])\}").unwrap());
regex.insert("process_sub",Regex::new(r"^>\(.*\)$").unwrap());
regex.insert("command_sub",Regex::new(r"^\$\([^\)]+\)$").unwrap());
regex.insert("arithmetic",Regex::new(r"^\$\(\([^\)]+\)\)$").unwrap());
regex.insert("subshell",Regex::new(r"^\([^\)]+\)$").unwrap());
regex.insert("test",Regex::new(r"^\[\s*(.*?)\s*\]$").unwrap());
regex.insert("sng_string",Regex::new(r#"^\'([^\']*)\'$"#).unwrap());
regex.insert("dub_string",Regex::new(r#"^\"([^\"]*)\"$"#).unwrap());
regex.insert("var_sub",Regex::new(r"\$(?:([A-Za-z_][A-Za-z0-9_]*)|\{([A-Za-z_][A-Za-z0-9_]*)\})").unwrap());
regex.insert("assignment",Regex::new(r"^[A-Za-z_][A-Za-z0-9_]*=.*$").unwrap());
regex.insert("funcdef",Regex::new(r"^[\x20-\x7E]*\(\)\s+\{[\s\S]*?\}").unwrap());
regex.insert("operator",Regex::new(r"(?:&&|\|\||[><]=?|[|&])").unwrap());
regex.insert("cmdsep",Regex::new(r"^(?:\n|;)$").unwrap());
regex.insert("ident",Regex::new(r"^[\x20-\x7E]*$").unwrap());
regex
});
pub const KEYWORDS: [&str;14] = [
"if", "while", "until", "for", "case", "select",
"then", "elif", "else", "in",
"do", "done", "fi", "esac"
];
pub const OPENERS: [&str;6] = [
"if", "while", "until", "for", "case", "select",
];
pub const BUILTINS: [&str; 22] = [
"echo", "jobs", "unset", "fg", "bg", "set", "builtin", "test", "[", "shift", "alias", "export", "cd", "readonly", "declare", "local", "unset", "trap", "node", "exec", "source", "wait",
];
pub const CMDSEP: [char;2] = [
';', '\n'
];
pub const WHITESPACE: [char;2] = [
' ', '\t'
];
bitflags! {
#[derive(Debug,Clone,Copy,PartialEq,Eq)]
pub struct FnFlags: u32 {
const RECURSE = 0b0001;
}
#[derive(Debug,Hash,Clone,Copy,PartialEq,Eq)]
pub struct WdFlags: u32 {
const KEYWORD = 0b000000000000000001;
const BUILTIN = 0b000000000000000010;
const FUNCTION = 0b000000000000000100;
const ALIAS = 0b000000000000001000;
const IS_ARG = 0b000000000000010000;
const DUB_QUOTED = 0b000000000000100000;
const SNG_QUOTED = 0b000000000001000000;
const IN_BRACE = 0b000000000010000000;
const IN_PAREN = 0b000000000100000000;
const IS_SUB = 0b000000001000000000;
const IS_OP = 0b000000010000000000;
const EXPECT_IN = 0b000000100000000000;
const IS_PATH = 0b000001000000000000;
const FROM_ALIAS = 0b000010000000000000;
const FROM_FUNC = 0b000100000000000000;
const FROM_VAR = 0b001000000000000000;
}
}
macro_rules! define_expectations {
($($name:expr => $pattern:expr),* $(,)?) => {{
use crate::interp::token::TkState::*;
let mut m = HashMap::new();
$(m.insert($name, $pattern);)*
m
}};
}
#[derive(Debug,Hash,Clone,PartialEq,Eq)]
pub struct WordDesc {
pub text: String,
pub span: Span,
pub flags: WdFlags
}
impl WordDesc {
pub fn empty() -> Self {
Self {
text: String::new(),
span: Span::new(),
flags: WdFlags::empty()
}
}
pub fn set_span(self, span: Span) -> Self {
Self {
text: self.text,
span,
flags: self.flags
}
}
pub fn add_char(&mut self, ch: char) -> Self {
trace!("cloning text: {}",self.text);
let mut text = std::mem::take(&mut self.text);
trace!("cloned text: {}",text);
trace!("pushing char: {}",ch);
text.push(ch);
trace!("after pushing: {}",text);
Self {
text,
span: Span::from(self.span.start, self.span.end),
flags: self.flags,
}
}
pub fn cat_string(mut self, s: &str) -> Self {
self.text.push_str(s);
Self {
text: self.text,
span: Span::from(self.span.start, self.span.end + 1),
flags: self.flags
}
}
pub fn contains_flag(&self, flag: WdFlags) -> bool {
self.flags.contains(flag)
}
pub fn add_flag(self, flag: WdFlags) -> Self {
let mut flags = self.flags;
flags |= flag;
let new = Self {
text: self.text,
span: self.span,
flags
};
trace!("added flag: {:?}, new flags {:?}",flag,new.flags);
new
}
pub fn remove_flag(self, flag: WdFlags) -> Self {
let mut flags = self.flags;
flags &= !flag;
Self {
text: self.text,
span: self.span,
flags
}
}
pub fn toggle_flag(self, flag: WdFlags) -> Self {
let mut flags = self.flags;
flags ^= flag;
Self {
text: self.text,
span: self.span,
flags
}
}
pub fn reset_flags(self) -> Self {
Self {
text: self.text,
span: self.span,
flags: WdFlags::empty()
}
}
pub fn push_span(&mut self, count: usize) {
self.span = Span::from(self.span.start,self.span.end + count);
}
pub fn push_span_start(&mut self, count: usize) {
let mut span_start = self.span.start;
let mut span_end = self.span.end;
span_start += count;
if span_end < span_start {
span_end = span_start
}
self.span = Span::from(span_start,span_end);
}
pub fn delimit(self, delim: char) -> Self {
let flag = match delim {
'{' => WdFlags::IN_BRACE,
'(' => WdFlags::IN_PAREN,
_ => unreachable!()
};
self.add_flag(flag)
}
}
impl Default for WordDesc {
fn default() -> Self {
Self::empty()
}
}
#[derive(Debug,Hash,Clone,PartialEq,Eq)]
pub struct Tk {
pub tk_type: TkType,
pub wd: WordDesc
}
#[derive(Debug,Hash,Clone,PartialEq,Eq)]
pub enum TkType {
// Control Flow Keywords
If,
Then,
Else,
Elif,
Fi,
For,
While,
Until,
Do,
Done,
Case,
Esac,
Select,
In,
FuncBody,
Redirection { redir: Redir },
FuncDef,
Assignment, // `=`
LogicAnd, // `&&`
LogicOr, // `||`
Pipe, // `|`
PipeBoth, // '|&'
Background, // `&`
// Grouping and Subshells
ProcessSub,
Subshell, // `(`
Array { elements: Vec<Tk> },
Vars { vars: Vec<Tk> },
// Strings and Identifiers
String, // Generic string literal
Ident, // Identifier for variables, functions, etc.
Expanded, // Token from an expansion
// Expansions
BraceExpansion,
VariableSub, // `$var`, `${var}`
CommandSub, // `$(command)`
// Comments
Comment, // `#`
// Special Characters
Cmdsep, // ';' or '\n'
CasePat,
Whitespace, // Space or tab
SOI,
EOI,
}
impl Tk {
pub fn new(text: String, span: Span, flags: WdFlags) -> Self {
Self {
tk_type: TkType::String,
wd: WordDesc {
text,
span,
flags
}
}
}
pub fn from(mut wd: WordDesc, context: TkState) -> Result<Self,ShellError> {
use crate::interp::token::TkState::*;
use crate::interp::token::TkType as TkT;
match context {
Root => panic!("not supposed to be here"),
Ident => Ok(Tk { tk_type: TkT::Ident, wd }),
Arg => Ok(Tk { tk_type: TkT::Ident, wd: wd.add_flag(WdFlags::IS_ARG) }),
Command => Ok(Tk { tk_type: TkT::Ident, wd }),
Array => Ok(Tk { tk_type: TkT::Array { elements: vec![] }, wd }),
If => Ok(Tk { tk_type: TkT::If, wd: wd.add_flag(WdFlags::KEYWORD) }),
For => Ok(Tk { tk_type: TkT::For, wd: wd.add_flag(WdFlags::KEYWORD) }),
Loop => Ok({
if wd.text == "while" {
Tk { tk_type: TkT::While, wd: wd.add_flag(WdFlags::KEYWORD) }
} else if wd.text == "until" {
Tk { tk_type: TkT::Until, wd: wd.add_flag(WdFlags::KEYWORD) }
} else { unreachable!("reached loop context with this: {}",wd.text) }
}),
Case => Ok(Tk { tk_type: TkT::Case, wd: wd.add_flag(WdFlags::KEYWORD) }),
Select => Ok(Tk { tk_type: TkT::Select, wd: wd.add_flag(WdFlags::KEYWORD) }),
In | CaseIn => Ok(Tk { tk_type: TkT::In, wd: wd.add_flag(WdFlags::KEYWORD) }),
CasePat => Ok(Tk { tk_type: TkT::Ident, wd }),
Elif => Ok(Tk { tk_type: TkT::Elif, wd: wd.add_flag(WdFlags::KEYWORD) }),
Else => Ok(Tk { tk_type: TkT::Else, wd: wd.add_flag(WdFlags::KEYWORD) }),
Do => Ok(Tk { tk_type: TkT::Do, wd: wd.add_flag(WdFlags::KEYWORD) }),
Then => Ok(Tk { tk_type: TkT::Then, wd: wd.add_flag(WdFlags::KEYWORD) }),
Done => Ok(Tk { tk_type: TkT::Done, wd: wd.add_flag(WdFlags::KEYWORD) }),
Fi => Ok(Tk { tk_type: TkT::Fi, wd: wd.add_flag(WdFlags::KEYWORD) }),
Esac => Ok(Tk { tk_type: TkT::Esac, wd: wd.add_flag(WdFlags::KEYWORD) }),
Subshell => Ok(Tk { tk_type: TkT::Subshell, wd }),
SQuote => Ok(Tk { tk_type: TkT::String, wd }),
DQuote => Ok(Tk { tk_type: TkT::String, wd }),
Redirect => todo!(),
CommandSub => Ok(Tk { tk_type: TkT::CommandSub, wd }),
Separator => Ok(Tk { tk_type: TkT::Cmdsep, wd }),
_ => return Err(ShellError::from_parse(format!("Parse error: {}", wd.text).as_str(), wd.span))
}
}
pub fn start_of_input() -> Self {
Tk {
tk_type: TkType::SOI,
wd: WordDesc { text: "".into(), span: Span::from(0,0), flags: WdFlags::empty() }
}
}
pub fn end_of_input(end: usize) -> Self {
Tk {
tk_type: TkType::EOI,
wd: WordDesc { text: "".into(), span: Span::from(end,end), flags: WdFlags::empty() }
}
}
pub fn cmdsep(wd: &WordDesc, pos: usize) -> Self {
Tk {
tk_type: TkType::Cmdsep,
wd: WordDesc { text: wd.text.clone(), span: Span::from(pos + 1,pos + 1), flags: WdFlags::empty() }
}
}
fn get_keyword_token(wd: &WordDesc) -> Result<TkType,ShellError> {
let text = wd.text.clone();
match text.as_str() {
"if" => Ok(TkType::If),
"elif" => Ok(TkType::Elif),
"else" => Ok(TkType::Else),
"then" => Ok(TkType::Then),
"fi" => Ok(TkType::Fi),
"for" => Ok(TkType::For),
"while" => Ok(TkType::While),
"until" => Ok(TkType::Until),
"do" => Ok(TkType::Do),
"done" => Ok(TkType::Done),
"case" => Ok(TkType::Case),
"esac" => Ok(TkType::Esac),
"select" => Ok(TkType::Select),
"in" => Ok(TkType::In),
_ => Err(ShellError::from_parse(format!("Unrecognized keyword: {}",wd.text).as_str(), wd.span))
}
}
fn split_func(wd: &WordDesc) -> Result<TkType,ShellError> {
let func_raw = &wd.text;
let (mut name,mut body) = func_raw.split_once(' ').unwrap();
name = name.trim();
body = body.trim();
if name.ends_with("()") {
name = name.strip_suffix("()").unwrap();
name = name.trim();
}
if body.starts_with('{') && body.ends_with('}') {
body = body.strip_prefix('{').unwrap();
body = body.strip_suffix('}').unwrap();
body = body.trim();
} else {
return Err(ShellError::from_internal(format!("This body made it to split_func with no surrounding braces: {}",body).as_str()));
}
Ok(TkType::FuncDef)
}
fn get_operator_type(word_desc: &WordDesc) -> TkType {
match word_desc.text.as_str() {
"|&" => TkType::PipeBoth,
"&" => TkType::Background,
"&&" => TkType::LogicAnd,
"||" => TkType::LogicOr,
"|" => TkType::Pipe,
_ => unreachable!()
}
}
pub fn text(&self) -> &str {
self.wd.text.as_str()
}
pub fn span(&self) -> Span {
self.wd.span
}
pub fn class(&self) -> TkType {
self.tk_type.clone()
}
pub fn flags(&self) -> WdFlags {
self.wd.flags
}
}
#[derive(Debug,Hash,Clone,PartialEq,Eq)]
pub struct Redir {
pub fd_source: i32,
pub op: RedirType,
pub fd_target: Option<i32>,
pub file_target: Option<Box<Tk>>
}
#[derive(Debug,Hash,Clone,PartialEq,Eq)]
pub enum RedirType {
Output,
Append,
Input,
Heredoc,
Herestring
}
#[derive(Eq,Hash,PartialEq)]
pub enum TkState {
Root, // Outer contex, allows for anything and everything. Closed by the EOI token
ArrDec, // Used in arrays like for i in ArrDec1 ArrDec2
VarDec, // Used in for loop vars like for VarDec1 VarDec2 in array
SingleVarDec, // Used in case and select
Ident, // Generic words used for var and arr declarations
Arg, // Command arguments; only appear after commands
Command, // Starting point for the tokenizer
FuncDef, // defining a function like() { this }
FuncBody, // The stuff { inside braces }
Array, // Used in for loops and select statements
If, // If statement opener
For, // For loop opener
Loop, // While/Until opener
Case, // Case opener
Select, // Select opener
In, // Used in for, case, and select statements
CaseBlock, // this)kind of thing;;
CaseIn, // 'In' context used for case statements, signaling the tokenizer to look for stuff 'like)this;;esac'
CasePat, // the left side of this)kind of thing
CaseBody, // the right side of this)kind of thing
Elif, // Secondary if/then blocks
Else, // Else statements
Do, // Select, for, and while/until condition/body separator
Then, // If statement condition/body separator
Done, // Select, for, and while/until closer
Fi, // If statement closer
Esac, // Case statement closer
Subshell, // Subshells, look (like this)
SQuote, // 'Single quoted strings'
DQuote, // "Double quoted strings"
Escaped, // Used to denote an escaped character like \a
Redirect, // >, <, <<, <<<, 1>&2, 2>, etc.
Comment, // #Comments like this
Whitespace, // Space or tabs
CommandSub, // $(Command substitution)
Operator, // operators
Separator, // Semicolon or newline to end an invocation
DeadEnd, // Used for closing keywords like 'fi' and 'done' that demand a separator immediately after
Invalid // Used when an unexpected state is discovered
}
impl TkState {
pub fn check_str(slice: &str, context: TkState) -> Self {
use crate::interp::token::TkState::*;
match slice {
// Loop and conditional openers
"for" if matches!(context, Command) => For,
"if" if matches!(context, Command) => If,
"while" if matches!(context, Command) => Loop,
"until" if matches!(context, Command) => Loop,
// Conditional separators and terminators
"then" if matches!(context, If | Elif) => Then,
"elif" if matches!(context, Then | Elif) => Elif,
"else" if matches!(context, If | Then | Elif) => Else,
"fi" if matches!(context, If | Then | Elif | Else) => Fi,
// Loop terminators
"do" if matches!(context, For | Loop) => Do,
"done" if matches!(context, Do) => Done,
// Case-specific keywords
"case" if matches!(context, Command) => Case,
"in" if matches!(context, Case | For | Select) => In,
"esac" if matches!(context, CaseIn | CaseBlock | CaseBody) => Esac,
// Select-specific keywords
"select" if matches!(context, Command) => Select,
// General flow control
"in" if matches!(context, For | Case | Select) => In,
"|" if Self::executable().contains(&context) => Operator,
"&" if Self::executable().contains(&context) => Operator,
// Defaults to Ident for non-keyword or unmatched cases
_ => Ident,
}
}
pub fn executable() -> Vec<Self> {
use crate::interp::token::TkState::*;
let mut execs = vec![
Command,
Subshell
];
execs.extend(Self::openers());
execs
}
pub fn body() -> Vec<Self> {
use crate::interp::token::TkState::*;
vec![
Command,
Arg,
Separator,
Redirect,
SQuote,
DQuote,
CommandSub,
Subshell
]
}
pub fn openers() -> Vec<Self> {
use crate::interp::token::TkState::*;
vec![
For,
Loop,
If,
Case,
Select
]
}
}
pub struct RshTokenizer {
input: String,
char_stream: VecDeque<char>,
context: TkState,
pub tokens: Vec<Tk>,
pub span: Span,
}
impl RshTokenizer {
pub fn new(input: &str) -> Self {
let input = input.trim().to_string();
let char_stream = input.chars().collect::<VecDeque<char>>();
let tokens = vec![Tk { tk_type: TkType::SOI, wd: WordDesc::empty() }];
Self { input, char_stream, context: TkState::Command, tokens, span: Span::new() }
}
fn advance(&mut self) -> Option<char> {
self.span.end += 1;
self.char_stream.pop_front()
}
pub fn tokenize(&mut self) -> Result<(),ShellError> {
use crate::interp::token::TkState::*;
let mut wd = WordDesc::empty();
while !self.char_stream.is_empty() {
self.span.start = self.span.end;
wd = wd.set_span(self.span);
if self.context == DeadEnd && !matches!(self.char_stream.front().unwrap(),';' | '\n') {
return Err(ShellError::from_parse("Expected a semicolon or newline here", self.span))
}
match self.char_stream.front().unwrap() {
'\\' => {
let escape = self.advance().unwrap();
wd = wd.add_char(escape);
let escaped_char = self.advance();
if let Some(ch) = escaped_char {
wd = wd.add_char(ch)
}
}
';' | '\n' => {
self.advance();
self.tokens.push(Tk::cmdsep(&wd,self.span.end));
self.context = Command;
continue
}
'#' => self.context = Comment,
'(' if self.context == Command => self.context = Subshell,
'{' if self.context == FuncDef => self.context = FuncBody,
'\'' if matches!(self.context, Command | Arg) => self.context = SQuote,
'"' if matches!(self.context, Command | Arg) => self.context = DQuote,
'$' if matches!(self.context, Command | Arg) => {
let dollar = self.advance().unwrap();
if self.char_stream.front().is_some_and(|ch| *ch == '(') {
self.context = CommandSub
}
self.char_stream.push_front(dollar);
self.span.end -= 1;
}
' ' | '\t' => {
self.advance();
continue
}
_ => { /* Do nothing */ }
}
match self.context {
Command => self.command_context(take(&mut wd)),
Arg => self.arg_context(take(&mut wd)),
DQuote | SQuote => self.string_context(take(&mut wd)),
VarDec => self.vardec_context(take(&mut wd))?,
SingleVarDec => self.vardec_context(take(&mut wd))?,
ArrDec => self.arrdec_context(take(&mut wd))?,
Subshell | CommandSub => self.subshell_context(take(&mut wd)),
FuncBody => self.func_context(take(&mut wd)),
Case => self.case_context(take(&mut wd))?,
Comment => {
while self.char_stream.front().is_some_and(|ch| *ch != '\n') {
self.advance();
}
self.advance(); // Consume the newline too
self.context = Command
}
_ => unreachable!()
}
}
self.tokens.push(Tk::end_of_input(self.input.len()));
self.clean_tokens();
Ok(())
}
fn command_context(&mut self, mut wd: WordDesc) {
use crate::interp::token::TkState::*;
wd = self.complete_word(wd);
if wd.text.ends_with("()") {
wd.text = wd.text.strip_suffix("()").unwrap().to_string();
self.tokens.push(Tk { tk_type: TkType::FuncDef, wd });
self.context = FuncDef;
} else if KEYWORDS.contains(&wd.text.as_str()) {
match wd.text.as_str() {
"then" | "if" | "elif" | "else" | "do" | "while" | "until" => self.context = Command,
"for" => self.context = VarDec,
"case" => self.context = Case,
"select" => self.context = Select,
_ => self.context = DeadEnd
}
match wd.text.as_str() {
"if" => self.tokens.push(Tk { tk_type: TkType::If, wd }),
"then" => self.tokens.push(Tk { tk_type: TkType::Then, wd }),
"elif" => self.tokens.push(Tk { tk_type: TkType::Elif, wd }),
"else" => self.tokens.push(Tk { tk_type: TkType::Else, wd }),
"fi" => self.tokens.push(Tk { tk_type: TkType::Fi, wd }),
"for" => self.tokens.push(Tk { tk_type: TkType::For, wd }),
"do" => self.tokens.push(Tk { tk_type: TkType::Do, wd }),
"done" => self.tokens.push(Tk { tk_type: TkType::Done, wd }),
"while" => self.tokens.push(Tk { tk_type: TkType::While, wd }),
"until" => self.tokens.push(Tk { tk_type: TkType::Until, wd }),
"case" => self.tokens.push(Tk { tk_type: TkType::Case, wd }),
"select" => self.tokens.push(Tk { tk_type: TkType::Select, wd }),
_ => unreachable!("text: {}", wd.text)
}
} else if matches!(wd.text.as_str(), ";" | "\n") || self.char_stream.is_empty() {
let flags = match self.context {
Arg => WdFlags::IS_ARG,
Command => {
if BUILTINS.contains(&wd.text.as_str()) {
WdFlags::BUILTIN
} else {
WdFlags::empty()
}
}
_ => unreachable!()
};
if wd.text.has_unescaped("=") {
self.tokens.push(Tk { tk_type: TkType::Assignment, wd });
self.context = Arg
} else {
self.tokens.push(Tk { tk_type: TkType::Ident, wd: wd.reset_flags().add_flag(flags) });
self.context = Arg
}
self.context = Command;
} else {
let flags = match self.context {
Arg => WdFlags::IS_ARG,
Command => {
if BUILTINS.contains(&wd.text.as_str()) {
WdFlags::BUILTIN
} else {
WdFlags::empty()
}
}
_ => unreachable!()
};
if wd.text.has_unescaped("=") {
self.tokens.push(Tk { tk_type: TkType::Assignment, wd });
self.context = Arg
} else {
self.tokens.push(Tk { tk_type: TkType::Ident, wd: wd.reset_flags().add_flag(flags) });
self.context = Arg
}
}
}
fn arg_context(&mut self, mut wd: WordDesc) {
wd = self.complete_word(wd);
match wd.text.as_str() {
"||" | "&&" | "|" | "|&" => {
while self.char_stream.front().is_some_and(|ch| *ch == '\n') {
self.advance(); // Allow line continuation
}
// Push the token
}
_ => { /* Do nothing */ }
}
match wd.text.as_str() {
"||" => {
self.tokens.push(Tk { tk_type: TkType::LogicOr, wd: wd.add_flag(WdFlags::IS_OP) });
self.context = TkState::Command
}
"&&" => {
self.tokens.push(Tk { tk_type: TkType::LogicAnd, wd: wd.add_flag(WdFlags::IS_OP) });
self.context = TkState::Command
}
"|" => {
self.tokens.push(Tk { tk_type: TkType::Pipe, wd: wd.add_flag(WdFlags::IS_OP) });
self.context = TkState::Command
}
"|&" => {
self.tokens.push(Tk { tk_type: TkType::PipeBoth, wd: wd.add_flag(WdFlags::IS_OP) });
self.context = TkState::Command
}
"&" => {
self.tokens.push(Tk { tk_type: TkType::Background, wd: wd.add_flag(WdFlags::IS_OP) });
self.context = TkState::Command
}
";" | "\n" => {
self.tokens.push(Tk::cmdsep(&wd,wd.span.start));
self.context = TkState::Command
}
_ if REGEX["redirection"].is_match(&wd.text) => {
let mut fd_out;
let operator;
let fd_target;
if let Some(caps) = REGEX["redirection"].captures(&wd.text) {
fd_out = caps.name("fd_out").and_then(|fd| fd.as_str().parse::<i32>().ok()).unwrap_or(1);
operator = caps.name("operator").unwrap().as_str();
fd_target = caps.name("fd_target").and_then(|fd| fd.as_str().parse::<i32>().ok())
} else { unreachable!() }
let op = match operator {
">" => RedirType::Output,
">>" => RedirType::Append,
"<" => RedirType::Input,
"<<<" => RedirType::Herestring,
_ => unimplemented!()
};
if matches!(op, RedirType::Input | RedirType::Herestring) {
fd_out = 0
}
let redir = Redir {
fd_source: fd_out,
op,
fd_target,
file_target: None
};
self.tokens.push(Tk { tk_type: TkType::Redirection { redir }, wd })
}
_ => {
self.tokens.push(Tk { tk_type: TkType::Ident, wd: wd.add_flag(WdFlags::IS_ARG) });
}
}
}
fn string_context(&mut self, mut wd: WordDesc) {
// Pop the opening quote
self.advance();
while let Some(ch) = self.advance() {
match ch {
'\\' => {
wd = wd.add_char(ch);
let next_char = self.advance();
if let Some(ch) = next_char {
wd = wd.add_char(ch)
}
}
'"' if self.context == TkState::DQuote => {
self.context = TkState::Arg;
break
}
'\'' if self.context == TkState::SQuote => {
self.context = TkState::Arg;
break
}
_ => {
wd = wd.add_char(ch)
}
}
}
wd.span = self.span;
self.tokens.push(Tk { tk_type: TkType::String, wd })
}
fn vardec_context(&mut self, mut wd: WordDesc) -> Result<(),ShellError> {
let mut found = false;
loop {
let span = wd.span;
if self.char_stream.is_empty() {
return Err(ShellError::from_parse("Did not find an `in` keyword for this statement", span))
}
wd = self.complete_word(wd);
match wd.text.as_str() {
"in" => {
if !found {
return Err(ShellError::from_parse("Did not find a variable for this statement", wd.span))
}
self.tokens.push(Tk { tk_type: TkType::In, wd });
break
}
_ => {
if self.context == TkState::SingleVarDec && found {
return Err(ShellError::from_parse(format!("Only expected one variable in this statement, found: {}",wd.text).as_str(), wd.span))
}
found = true;
self.tokens.push(Tk { tk_type: TkType::Ident, wd: take(&mut wd) });
wd = wd.set_span(span);
}
}
}
self.context = TkState::ArrDec;
Ok(())
}
fn arrdec_context(&mut self, mut wd: WordDesc) -> Result<(),ShellError> {
let mut found = false;
while self.char_stream.front().is_some_and(|ch| !matches!(ch, ';' | '\n')) {
found = true;
wd = self.complete_word(wd);
wd.span = self.span;
self.tokens.push(Tk { tk_type: TkType::Ident, wd: take(&mut wd) });
wd = wd.set_span(self.span)
}
if self.char_stream.front().is_some_and(|ch| matches!(ch, ';' | '\n')) {
if !found {
return Err(ShellError::from_parse("Did not find any array elements for this statement", wd.span))
}
self.advance();
self.tokens.push(Tk::cmdsep(&wd,self.span.end + 1))
}
self.context = TkState::Command;
Ok(())
}
fn subshell_context(&mut self, mut wd: WordDesc) {
self.advance();
if self.context == TkState::CommandSub { self.advance(); }
let mut paren_stack = vec!['('];
while let Some(ch) = self.advance() {
match ch {
'(' => {
paren_stack.push(ch);
wd = wd.add_char(ch)
}
')' => {
paren_stack.pop();
if paren_stack.is_empty() {
break
}
wd = wd.add_char(ch);
}
_ => {
wd = wd.add_char(ch)
}
}
}
wd.span = self.span;
let tk = match self.context {
TkState::Subshell => {
Tk {
tk_type: TkType::Subshell,
wd
}
}
TkState::CommandSub => {
Tk {
tk_type: TkType::CommandSub,
wd
}
}
_ => unreachable!()
};
self.tokens.push(tk);
self.context = TkState::Arg;
}
fn func_context(&mut self, mut wd: WordDesc) {
self.advance();
if self.context == TkState::CommandSub { self.advance(); }
let mut brace_stack = vec!['{'];
while let Some(ch) = self.advance() {
match ch {
'{' => {
brace_stack.push(ch);
wd = wd.add_char(ch)
}
'}' => {
brace_stack.pop();
if brace_stack.is_empty() {
break
}
wd = wd.add_char(ch);
}
_ => {
wd = wd.add_char(ch)
}
}
}
wd.span = self.span;
wd.text = wd.text.trim().to_string();
self.tokens.push(Tk { tk_type: TkType::FuncBody, wd })
}
fn case_context(&mut self, mut wd: WordDesc) -> Result<(), ShellError> {
use crate::interp::token::TkState::*;
let span = wd.span;
self.context = SingleVarDec;
self.vardec_context(take(&mut wd))?;
if self.char_stream.front().is_some_and(|ch| matches!(*ch, ';' | '\n')) {
self.advance();
self.tokens.push(Tk::cmdsep(&wd,span.end + 1));
}
while !self.char_stream.is_empty() {
// Get pattern
let mut pat = String::new();
while let Some(ch) = self.advance() {
if ch == ')' { break }
pat.push(ch);
}
wd.text = pat;
if wd.text == "esac" {
self.tokens.push(Tk { tk_type: TkType::Esac, wd: take(&mut wd) });
break
}
let span = wd.span;
self.tokens.push(Tk { tk_type: TkType::CasePat, wd: take(&mut wd) });
// Get body
let mut body = String::new();
let mut prev_char = None;
while let Some(ch) = self.advance() {
if ch == ';' && prev_char == Some(';') {
break
}
prev_char = Some(ch);
body.push(ch);
}
let mut body_tokenizer = RshTokenizer::new(&body);
body_tokenizer.tokenize()?;
let len = body_tokenizer.tokens.len();
let body_tokens = Vec::from(&body_tokenizer.tokens[1..len-1]); // Strip SOI/EOI tokens
self.tokens.extend(body_tokens);
wd = wd.set_span(span);
}
self.context = DeadEnd;
Ok(())
}
fn complete_word(&mut self, mut wd: WordDesc) -> WordDesc {
let mut dub_quote = false;
let mut sng_quote = false;
while let Some(ch) = self.advance() {
if matches!(ch, '\'') && !dub_quote {
// Single quote handling
sng_quote = !sng_quote;
wd = wd.add_char(ch);
} else if matches!(ch, '"') && !sng_quote {
// Double quote handling
dub_quote = !dub_quote;
wd = wd.add_char(ch);
} else if dub_quote || sng_quote {
// Inside a quoted string
wd = wd.add_char(ch);
} else if !matches!(ch, ' ' | '\t' | '\n' | ';') {
// Regular character
wd = wd.add_char(ch);
} else if matches!(ch, '\n' | ';') {
// Preserve cmdsep for tokenizing
self.char_stream.push_front(ch);
self.span.end -= 1;
wd.span = self.span;
break;
} else {
// Whitespace handling
self.char_stream.push_front(ch);
while self.char_stream.front().is_some_and(|c| matches!(c, ' ' | '\t')) {
self.advance();
}
break;
}
}
wd
}
fn clean_tokens(&mut self) {
let mut buffer = VecDeque::new();
let mut tokens = VecDeque::from(take(&mut self.tokens));
while let Some(token) = tokens.pop_front() {
if matches!(token.tk_type, TkType::SOI | TkType::EOI | TkType::Cmdsep) || !token.text().is_empty() {
buffer.push_back(token);
}
}
self.tokens.extend(buffer.drain(..));
}
}

View File

@@ -0,0 +1,168 @@
//pub mod event;
//pub mod prompt;
//pub mod parser;
//mod rsh;
//
//use log::debug;
//
//use tokio::io::{self, AsyncBufReadExt, BufReader};
//use tokio_stream::{wrappers::LinesStream, StreamExt};
//
//use crate::event::EventLoop;
//
//#[tokio::main]
//async fn main() {
//env_logger::init();
//let mut event_loop = EventLoop::new();
//
//debug!("Starting event loop");
//TODO: use the value returned by this for something
//let _ = event_loop.listen().await;
//}
pub mod prompt;
pub mod event;
pub mod execute;
pub mod shellenv;
pub mod interp;
pub mod builtin;
pub mod comp;
use std::{env, fs::File, io::Read, os::fd::{AsFd, BorrowedFd}};
use event::{EventLoop, ShellError, ShellErrorFull};
use execute::{NodeWalker, RshWaitStatus};
use interp::parse::descend;
use libc::STDERR_FILENO;
use log::info;
use nix::unistd::write;
use shellenv::EnvFlags;
//use crate::event::EventLoop;
use crate::shellenv::ShellEnv;
#[tokio::main]
async fn main() {
let args = env::args().collect::<Vec<String>>();
let mut shellenv = ShellEnv::new(EnvFlags::empty());
if args[0].starts_with('-') {
shellenv.source_profile().ok();
}
match args.len() {
1 => {
shellenv.set_flags(EnvFlags::INTERACTIVE);
main_interactive(shellenv).await;
},
_ => {
main_noninteractive(args,shellenv).await;
}
}
}
async fn main_noninteractive(args: Vec<String>, mut shellenv: ShellEnv) {
let stderr = unsafe { BorrowedFd::borrow_raw(STDERR_FILENO) };
let mut pos_params: Vec<String> = vec![];
let input;
// Input Handling
if args[1] == "-c" {
if args.len() < 3 {
write(stderr.as_fd(), b"Expected a command after '-c' flag\n").unwrap();
return;
}
input = args[2].clone(); // Store the command string
} else {
let script_name = &args[1];
if args.len() > 2 {
pos_params = args[2..].to_vec();
}
let file = File::open(script_name);
match file {
Ok(mut script) => {
let mut buffer = vec![];
if let Err(e) = script.read_to_end(&mut buffer) {
write(stderr.as_fd(), format!("Error reading file: {}\n", e).as_bytes()).unwrap();
return;
}
input = String::from_utf8_lossy(&buffer).to_string(); // Convert file contents to String
}
Err(e) => {
write(stderr.as_fd(), format!("Error opening file: {}\n", e).as_bytes()).unwrap();
return;
}
}
}
// Code Execution Logic
shellenv.set_last_input(&input);
for (index,param) in pos_params.into_iter().enumerate() {
let key = format!("{}",index + 1);
shellenv.set_parameter(key, param);
}
let state = descend(&input, &shellenv);
match state {
Ok(parse_state) => {
let mut walker = NodeWalker::new(parse_state.ast, &mut shellenv);
match walker.start_walk() {
Ok(code) => {
info!("Last exit status: {:?}", code);
if let RshWaitStatus::Fail { code, cmd, span } = code {
if code == 127 {
if let Some(cmd) = cmd {
let err = ShellErrorFull::from(shellenv.get_last_input(),ShellError::from_no_cmd(&cmd, span));
write(stderr, format!("{}", err).as_bytes()).unwrap();
}
}
}
}
Err(e) => {
let err = ShellErrorFull::from(shellenv.get_last_input(),e);
write(stderr.as_fd(), format!("{}", err).as_bytes()).unwrap();
}
}
}
Err(e) => {
let err = ShellErrorFull::from(shellenv.get_last_input(),e);
write(stderr.as_fd(), format!("{}", err).as_bytes()).unwrap();
}
}
}
async fn main_interactive(mut shellenv: ShellEnv) {
env_logger::init();
let mut event_loop = EventLoop::new(&mut shellenv);
let _ = event_loop.listen().await;
}
//#[tokio::main]
//async fn main() {
//env_logger::init();
//loop {
//let mut stdout = io::stdout();
//stdout.write_all(b"> ").await.unwrap();
//stdout.flush().await.unwrap();
//
//let stdin = io::stdin();
//let mut reader = BufReader::new(stdin);
//
//let mut input = String::new();
//reader.read_line(&mut input).await.unwrap();
//
//let trimmed_input = input.trim().to_string();
//trace!("Received input! {}",trimmed_input);
//let shellenv = ShellEnv::new(false,false);
//let state = descend(&input,&shellenv);
//if let Err(e) = state {
//println!("{}",e);
//} else {
//println!("debug tree:");
//println!("{}",state.unwrap().ast);
//}
//}
//}

View File

@@ -0,0 +1,103 @@
use crate::comp::RshHelper;
use crate::event::Signals;
use std::path::{Path, PathBuf};
use tokio::sync::mpsc;
use log::info;
use rustyline::{self, config::Configurer, error::ReadlineError, history::{DefaultHistory, History}, ColorMode, Config, EditMode, Editor};
use crate::{event::ShellEvent, shellenv::ShellEnv};
use crate::interp::parse::{descend, NdType};
use crate::interp::expand;
fn init_prompt(shellenv: &ShellEnv) -> Editor<RshHelper, DefaultHistory> {
let mut config = Config::builder();
// Read options from ShellEnv
let max_size = shellenv.get_shopt("max_hist").max(1000); // Default to 1000
let hist_dupes = shellenv.get_shopt("hist_ignore_dupes") != 0;
let comp_limit = shellenv.get_shopt("comp_limit").max(5); // Default to 5
let edit_mode = match shellenv.get_shopt("edit_mode") {
0 => EditMode::Emacs,
_ => EditMode::Vi,
};
let auto_hist = shellenv.get_shopt("auto_hist") != 0;
let prompt_highlight = match shellenv.get_shopt("prompt_highlight") {
0 => ColorMode::Disabled,
_ => ColorMode::Enabled,
};
let tab_stop = shellenv.get_shopt("tab_stop").max(1); // Default to at least 1
// Build configuration
config = config
.max_history_size(max_size)
.unwrap_or_else(|e| {
eprintln!("Invalid max history size: {}", e);
std::process::exit(1);
})
.history_ignore_dups(hist_dupes)
.unwrap()
.completion_prompt_limit(comp_limit)
.edit_mode(edit_mode)
.auto_add_history(auto_hist)
.color_mode(prompt_highlight)
.tab_stop(tab_stop);
let config = config.build();
// Initialize editor
let mut rl = Editor::with_config(config).unwrap_or_else(|e| {
eprintln!("Failed to initialize Rustyline editor: {}", e);
std::process::exit(1);
});
rl.set_completion_type(rustyline::CompletionType::List);
rl.set_helper(Some(RshHelper::new(shellenv)));
// Load history file
let hist_path = expand::expand_var(shellenv, "$HIST_FILE".into());
info!("history path in init_prompt(): {}",hist_path);
let hist_path = PathBuf::from(hist_path);
if let Err(e) = rl.load_history(&hist_path) {
eprintln!("No previous history found or failed to load history: {}", e);
}
rl
}
pub async fn prompt(sender: mpsc::Sender<ShellEvent>, shellenv: &mut ShellEnv) {
let mut rl = init_prompt(shellenv);
let hist_path = expand::expand_var(shellenv, "$HIST_FILE".into());
let prompt = expand::expand_prompt(shellenv);
match rl.readline(&prompt) {
Ok(line) => {
let _ = rl.history_mut().add(&line);
let _ = rl.history_mut().save(Path::new(&hist_path));
shellenv.set_last_input(&line);
let state = descend(&line,shellenv);
match state {
Ok(parse_state) => {
if let NdType::Root { deck } = &parse_state.ast.nd_type {
if !deck.is_empty() {
let _ = sender.send(ShellEvent::NewAST(parse_state.ast)).await;
}
}
}
Err(e) => {
let _ = sender.send(ShellEvent::CatchError(e)).await;
}
}
}
Err(ReadlineError::Interrupted) => {
let _ = sender.send(ShellEvent::Signal(Signals::SIGINT)).await;
}
Err(ReadlineError::Eof) => {
let _ = sender.send(ShellEvent::Signal(Signals::SIGQUIT)).await;
}
Err(e) => {
eprintln!("{:?}",e);
}
}
let _ = sender.send(ShellEvent::Prompt).await;
}

View File

@@ -0,0 +1,640 @@
use std::borrow::BorrowMut;
use std::env;
use std::fmt::Display;
use nix::sys::signal::{self, Signal};
use nix::unistd::{gethostname, Pid, User};
use nix::NixPath;
use std::collections::{HashSet,HashMap};
use std::ffi::CString;
use std::fs::File;
use std::io::Read;
use std::os::fd::RawFd;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use bitflags::bitflags;
use log::{debug, info, trace};
use crate::event::{ShellError, ShellErrorFull};
use crate::execute::{NodeWalker, RshWaitStatus};
use crate::interp::expand::expand_var;
use crate::interp::helper;
use crate::interp::parse::{descend, Node, Span};
bitflags! {
#[derive(Debug,Clone)]
pub struct JobFlags: i8 {
const LONG = 0b00000001;
const PIDS_ONLY = 0b00000010;
const NEW_ONLY = 0b00000100;
const RUNNING = 0b00001000;
const STOPPED = 0b00010000;
}
}
#[derive(Debug,Clone)]
pub struct Job {
job_id: i32,
pids: Vec<Pid>,
commands: Vec<String>,
pgid: Pid,
status: RshWaitStatus,
current: bool,
prev: bool,
}
impl Job {
pub fn new(job_id: i32, pids: Vec<Pid>, commands: Vec<String>, pgid: Pid) -> Self {
Self { job_id, pgid, pids, commands, status: RshWaitStatus::Running, current: true, prev: false }
}
pub fn status(&mut self, new_stat: RshWaitStatus) {
self.status = new_stat;
}
pub fn pids(&self) -> &[Pid] {
&self.pids
}
pub fn pgid(&self) -> &Pid {
&self.pgid
}
pub fn commands(&self) -> Vec<String> {
self.commands.clone()
}
pub fn get_status(&self) -> RshWaitStatus {
self.status.clone()
}
pub fn signal_proc(&self, sig: Signal) -> Result<(),ShellError> {
if self.pids().len() == 1 {
let pid = *self.pids().first().unwrap();
signal::kill(pid, sig).map_err(|_| ShellError::from_io())
} else {
signal::killpg(self.pgid, sig).map_err(|_| ShellError::from_io())
}
}
pub fn print(&self, long: bool) -> String {
let mut output = String::new();
// Add job ID and status
let symbol = if self.current { "+" } else if self.prev { "-" } else { " " };
output.push_str(&format!("[{}]{} \t", self.job_id, symbol));
let padding_num = symbol.len() + self.job_id.to_string().len() + 3;
let mut padding: String = " ".repeat(padding_num);
padding.push('\t');
// Add commands and PIDs
for (i, cmd) in self.commands.iter().enumerate() {
let mut cmd = cmd.clone();
if i != self.commands.len() - 1 {
cmd.push_str(" |");
}
if long {
// Long format includes PIDs
output.push_str(&format!(
"{}{} {} {}\n",
if i != 0 { padding.clone() } else { "".into() },
self.pids().get(i).unwrap(),
self.status,
cmd
));
} else {
// Short format only includes commands
output.push_str(&format!(
"{}{} {}\n",
if i != 0 { padding.clone() } else { "".into() },
self.status,
cmd
)
);
}
}
output
}
}
impl Display for Job {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Determine the job symbol
let symbol = if self.current { "+" } else if self.prev { "-" } else { "" };
// Write the job ID and symbol
write!(f, "[{}]{} ", self.job_id, symbol)?;
// Iterate over processes in the job
for i in 0..self.pids.len() {
let cmd = self.commands.get(i).unwrap();
// Add padding for subsequent lines
let padding = if i == 0 { "" } else { " " };
let cmd = if i == self.pids.len() - 1 { cmd } else { &format!("{}{}",cmd," |") };
writeln!(f, "{}\t{}", padding, cmd)?;
}
Ok(())
}
}
bitflags! {
#[derive(Debug,Copy,Clone,PartialEq)]
pub struct EnvFlags: u32 {
// Guard conditions against infinite alias/var/function recursion
const NO_ALIAS = 0b00000000000000000000000000000001;
const NO_VAR = 0b00000000000000000000000000000010;
const NO_FUNC = 0b00000000000000000000000000000100;
// Context
const IN_FUNC = 0b00000000000000000000000000001000; // Enables the `return` builtin
const INTERACTIVE = 0b00000000000000000000000000010000;
const CLEAN = 0b00000000000000000000000000100000; // Do not inherit env vars from parent
const NO_RC = 0b00000000000000000000000001000000;
// Options set by 'set' command
const EXPORT_ALL_VARS = 0b00000000000000000000000010000000; // set -a
const REPORT_JOBS_ASAP = 0b00000000000000000000000100000000; // set -b
const EXIT_ON_ERROR = 0b00000000000000000000001000000000; // set -e
const NO_GLOB = 0b00000000000000000000010000000000; // set -f
const HASH_CMDS = 0b00000000000000000000100000000000; // set -h
const ASSIGN_ANYWHERE = 0b00000000000000000001000000000000; // set -k
const ENABLE_JOB_CTL = 0b00000000000000000010000000000000; // set -m
const NO_EXECUTE = 0b00000000000000000100000000000000; // set -n
const ENABLE_RSHELL = 0b00000000000000001000000000000000; // set -r
const EXIT_AFTER_EXEC = 0b00000000000000010000000000000000; // set -t
const UNSET_IS_ERROR = 0b00000000000000100000000000000000; // set -u
const PRINT_INPUT = 0b00000000000001000000000000000000; // set -v
const STACK_TRACE = 0b00000000000010000000000000000000; // set -x
const EXPAND_BRACES = 0b00000000000100000000000000000000; // set -B
const NO_OVERWRITE = 0b00000000001000000000000000000000; // set -C
const INHERIT_ERR = 0b00000000010000000000000000000000; // set -E
const HIST_SUB = 0b00000000100000000000000000000000; // set -H
const NO_CD_SYMLINKS = 0b00000001000000000000000000000000; // set -P
const INHERIT_RET = 0b00000010000000000000000000000000; // set -T
}
}
impl PartialEq for ShellEnv {
fn eq(&self, other: &Self) -> bool {
// Compare the inner value of `output_buffer`
let self_output = self.output_buffer.lock().unwrap();
let other_output = other.output_buffer.lock().unwrap();
*self_output == *other_output
&& self.flags == other.flags
&& self.env_vars == other.env_vars
&& self.variables == other.variables
&& self.aliases == other.aliases
&& self.shopts == other.shopts
&& self.functions == other.functions
&& self.parameters == other.parameters
&& self.open_fds == other.open_fds
&& self.last_input == other.last_input
}
}
#[derive(Clone,Debug)]
pub struct JobTable {
jobs: HashMap<i32,Job>,
curr_job: Option<i32>,
prev_job: Option<i32>,
updated_since_check: HashMap<i32,Job>
}
impl JobTable {
fn new() -> Self {
Self {
jobs: HashMap::new(),
curr_job: None,
prev_job: None,
updated_since_check: HashMap::new()
}
}
pub fn print_jobs(&self, flags: &JobFlags) {
let jobs = self.jobs.values().collect::<Vec<&Job>>();
for job in jobs {
// Filter jobs based on flags
if flags.contains(JobFlags::RUNNING) && !matches!(job.status, RshWaitStatus::Running) {
continue;
}
if flags.contains(JobFlags::STOPPED) && !matches!(job.status, RshWaitStatus::Stopped {..}) {
continue;
}
if flags.contains(JobFlags::PIDS_ONLY) {
// Print only PIDs
for pid in &job.pids {
println!("{}", pid);
}
} else {
// Print the job in the selected format
println!("{}", job.print(flags.contains(JobFlags::LONG)));
}
}
}
}
#[derive(Debug,Clone)]
pub struct ShellEnv {
pub flags: EnvFlags,
pub output_buffer: Arc<Mutex<String>>,
pub env_vars: HashMap<String, String>,
pub variables: HashMap<String, String>,
pub aliases: HashMap<String, String>,
pub shopts: HashMap<String,usize>,
pub functions: HashMap<String, Box<Node>>,
pub parameters: HashMap<String, String>,
pub open_fds: HashSet<i32>,
pub last_input: Option<String>,
pub job_table: JobTable
}
impl ShellEnv {
// Constructor
pub fn new(flags: EnvFlags) -> Self {
let mut open_fds = HashSet::new();
let shopts = init_shopts();
// TODO: probably need to find a way to initialize env vars that doesnt rely on a parent process
let clean_env = flags.contains(EnvFlags::CLEAN);
let env_vars = Self::init_env_vars(clean_env);
open_fds.insert(0);
open_fds.insert(1);
open_fds.insert(2);
let mut shellenv = Self {
flags: EnvFlags::empty(),
output_buffer: Arc::new(Mutex::new(String::new())),
env_vars,
variables: HashMap::new(),
aliases: HashMap::new(),
shopts,
functions: HashMap::new(),
parameters: HashMap::new(),
open_fds,
last_input: None,
job_table: JobTable::new()
};
if !flags.contains(EnvFlags::NO_RC) {
let runtime_commands_path = &expand_var(&shellenv, "${HOME}/.rshrc".into());
let runtime_commands_path = Path::new(runtime_commands_path);
if runtime_commands_path.exists() {
if let Err(e) = shellenv.source_file(runtime_commands_path.to_path_buf()) {
let err = ShellErrorFull::from(shellenv.get_last_input(),e);
eprintln!("Failed to source ~/.rshrc: {}",err);
}
} else {
eprintln!("Warning: Runtime commands file '{}' not found.", runtime_commands_path.display());
}
}
shellenv
}
pub fn new_job(&mut self, pids: Vec<Pid>, commands: Vec<String>, pgid: Pid) {
let job_id = self.job_table.jobs.len() + 1;
let job = Job::new(job_id as i32,pids,commands,pgid);
println!("{}",job);
self.job_table.jobs.insert(job_id as i32, job);
self.set_curr_job(job_id as i32);
}
pub fn borrow_jobs(&mut self) -> &mut HashMap<i32,Job> {
&mut self.job_table.jobs
}
pub fn set_curr_job(&mut self, job_id: i32) {
self.job_table.prev_job = self.job_table.curr_job;
self.job_table.curr_job = Some(job_id);
}
fn init_env_vars(clean: bool) -> HashMap<String,String> {
let pathbuf_to_string = |pb: Result<PathBuf, std::io::Error>| pb.unwrap_or_default().to_string_lossy().to_string();
// First, inherit any env vars from the parent process if clean bit not set
let mut env_vars = HashMap::new();
if !clean {
env_vars = std::env::vars().collect::<HashMap<String,String>>();
}
let home;
let username;
let uid;
if let Some(user) = User::from_uid(nix::unistd::Uid::current()).ok().flatten() {
home = user.dir;
username = user.name;
uid = user.uid;
} else {
home = PathBuf::new();
username = "unknown".into();
uid = 0.into();
}
let home = pathbuf_to_string(Ok(home));
let hostname = gethostname().map(|hname| hname.to_string_lossy().to_string()).unwrap_or_default();
env_vars.insert("HOSTNAME".into(), hostname.clone());
env::set_var("HOSTNAME", hostname);
env_vars.insert("UID".into(), uid.to_string());
env::set_var("UID", uid.to_string());
env_vars.insert("TMPDIR".into(), "/tmp".into());
env::set_var("TMPDIR", "/tmp");
env_vars.insert("TERM".into(), "xterm-256color".into());
env::set_var("TERM", "xterm-256color");
env_vars.insert("LANG".into(), "en_US.UTF-8".into());
env::set_var("LANG", "en_US.UTF-8");
env_vars.insert("USER".into(), username.clone());
env::set_var("USER", username.clone());
env_vars.insert("LOGNAME".into(), username.clone());
env::set_var("LOGNAME", username);
env_vars.insert("PWD".into(), pathbuf_to_string(std::env::current_dir()));
env::set_var("PWD", pathbuf_to_string(std::env::current_dir()));
env_vars.insert("OLDPWD".into(), pathbuf_to_string(std::env::current_dir()));
env::set_var("OLDPWD", pathbuf_to_string(std::env::current_dir()));
env_vars.insert("HOME".into(), home.clone());
env::set_var("HOME", home.clone());
env_vars.insert("SHELL".into(), pathbuf_to_string(std::env::current_exe()));
env::set_var("SHELL", pathbuf_to_string(std::env::current_exe()));
env_vars.insert("HIST_FILE".into(),format!("{}/.rsh_hist",home));
env::set_var("HIST_FILE",format!("{}/.rsh_hist",home));
env_vars
}
pub fn mod_flags<F>(&mut self, transform: F) where F: FnOnce(&mut EnvFlags) {
transform(&mut self.flags)
}
pub fn is_interactive(&self) -> bool {
self.flags.contains(EnvFlags::INTERACTIVE)
}
pub fn set_last_input(&mut self, input: &str) {
self.last_input = Some(input.to_string())
}
pub fn get_last_input(&mut self) -> String {
self.last_input.clone().unwrap_or_default()
}
pub fn source_profile(&mut self) -> Result<(),ShellError> {
let home = self.get_variable("HOME").unwrap();
let path = PathBuf::from(format!("{}/.rsh_profile",home));
self.source_file(path)
}
pub fn source_file(&mut self, path: PathBuf) -> Result<(),ShellError> {
let mut file = File::open(&path).map_err(|_| ShellError::from_io())?;
let mut buffer = String::new();
file.read_to_string(&mut buffer).map_err(|_| ShellError::from_io())?;
self.last_input = Some(buffer.clone());
let state = descend(&buffer, self);
match state {
Ok(parse_state) => {
let mut walker = NodeWalker::new(parse_state.ast, self);
match walker.start_walk() {
Ok(code) => {
info!("Last exit status: {:?}", code);
if let RshWaitStatus::Fail { code, cmd, span } = code {
if code == 127 {
if let Some(cmd) = cmd {
let err = ShellErrorFull::from(self.get_last_input(),ShellError::from_no_cmd(&format!("Command not found: {}",cmd), span));
eprintln!("{}", err);
}
}
}
}
Err(e) => {
let err = ShellErrorFull::from(self.get_last_input(), e);
eprintln!("{}", err);
}
}
}
Err(e) => {
let err = ShellErrorFull::from(self.get_last_input(), e);
eprintln!("Encountered error while sourcing file: {}\n{}",path.display(), err);
}
}
Ok(())
}
pub fn change_dir(&mut self, path: &Path, span: Span) -> Result<(), ShellError> {
let result = helper::suppress_err(|| env::set_current_dir(path));
match result {
Ok(_) => {
let old_pwd = self.env_vars.remove("PWD").unwrap_or_default();
self.export_variable("OLDPWD".into(), old_pwd);
let new_dir = env::current_dir().unwrap().to_string_lossy().to_string();
self.export_variable("PWD".into(), new_dir);
Ok(())
}
Err(e) => Err(ShellError::from_execf(&e.to_string(), 1, span))
}
}
pub fn set_flags(&mut self, flags: EnvFlags) {
self.flags |= flags;
self.update_flags_param();
}
pub fn unset_flags(&mut self, flags:EnvFlags) {
self.flags &= !flags;
self.update_flags_param();
}
pub fn update_flags_param(&mut self) {
let mut flag_list = "abefhkmnrptuvxBCEHPT".chars();
let mut flag_string = String::new();
while let Some(ch) = flag_list.next() {
match ch {
'a' if self.flags.contains(EnvFlags::EXPORT_ALL_VARS) => flag_string.push(ch),
'b' if self.flags.contains(EnvFlags::REPORT_JOBS_ASAP) => flag_string.push(ch),
'e' if self.flags.contains(EnvFlags::EXIT_ON_ERROR) => flag_string.push(ch),
'f' if self.flags.contains(EnvFlags::NO_GLOB) => flag_string.push(ch),
'h' if self.flags.contains(EnvFlags::HASH_CMDS) => flag_string.push(ch),
'k' if self.flags.contains(EnvFlags::ASSIGN_ANYWHERE) => flag_string.push(ch),
'm' if self.flags.contains(EnvFlags::ENABLE_JOB_CTL) => flag_string.push(ch),
'n' if self.flags.contains(EnvFlags::NO_EXECUTE) => flag_string.push(ch),
'r' if self.flags.contains(EnvFlags::ENABLE_RSHELL) => flag_string.push(ch),
't' if self.flags.contains(EnvFlags::EXIT_AFTER_EXEC) => flag_string.push(ch),
'u' if self.flags.contains(EnvFlags::UNSET_IS_ERROR) => flag_string.push(ch),
'v' if self.flags.contains(EnvFlags::PRINT_INPUT) => flag_string.push(ch),
'x' if self.flags.contains(EnvFlags::STACK_TRACE) => flag_string.push(ch),
'B' if self.flags.contains(EnvFlags::EXPAND_BRACES) => flag_string.push(ch),
'C' if self.flags.contains(EnvFlags::NO_OVERWRITE) => flag_string.push(ch),
'E' if self.flags.contains(EnvFlags::INHERIT_ERR) => flag_string.push(ch),
'H' if self.flags.contains(EnvFlags::HIST_SUB) => flag_string.push(ch),
'P' if self.flags.contains(EnvFlags::NO_CD_SYMLINKS) => flag_string.push(ch),
'T' if self.flags.contains(EnvFlags::INHERIT_RET) => flag_string.push(ch),
_ => { /* Do nothing */ }
}
}
self.set_parameter("-".into(), flag_string);
}
pub fn open_fd(&mut self, fd: RawFd) {
self.open_fds.insert(fd);
}
pub fn close_fd(&mut self, fd: RawFd) {
self.open_fds.remove(&fd);
}
pub fn handle_exit_status(&mut self, wait_status: RshWaitStatus) {
match wait_status {
RshWaitStatus::Success { .. } => self.set_parameter("?".into(), "0".into()),
RshWaitStatus::Fail { code, cmd: _, span: _ } => self.set_parameter("?".into(), code.to_string()),
_ => unimplemented!()
}
}
pub fn set_interactive(&mut self, interactive: bool) {
if interactive {
self.flags |= EnvFlags::INTERACTIVE
} else {
self.flags &= !EnvFlags::INTERACTIVE
}
}
// Getters and Setters for `variables`
pub fn get_variable(&self, key: &str) -> Option<String> {
if let Some(value) = self.variables.get(key) {
Some(value.to_string())
} else if let Some(value) = self.env_vars.get(key) {
Some(value.to_string())
} else {
self.get_parameter(key).map(|val| val.to_string())
}
}
/// For C FFI calls
pub fn get_cvars(&self) -> Vec<CString> {
self.env_vars
.iter()
.map(|(key, value)| {
let env_pair = format!("{}={}", key, value);
CString::new(env_pair).unwrap() })
.collect::<Vec<CString>>()
}
pub fn set_variable(&mut self, key: String, mut value: String) {
debug!("inserted var: {} with value: {}",key,value);
if value.starts_with('"') && value.ends_with('"') {
value = value.strip_prefix('"').unwrap().into();
value = value.strip_suffix('"').unwrap().into();
}
self.variables.insert(key.clone(), value);
trace!("testing variable get: {} = {}", key, self.get_variable(key.as_str()).unwrap())
}
pub fn get_shopt(&self, key: &str) -> usize {
self.shopts[key]
}
pub fn set_shopt(&mut self, key: &str, value: usize) {
self.shopts.insert(key.into(),value);
}
pub fn export_variable(&mut self, key: String, value: String) {
let value = value.trim_matches(|ch| ch == '"').to_string();
if value.as_str() == "" {
self.variables.remove(&key);
self.env_vars.remove(&key);
} else {
self.variables.insert(key.clone(),value.clone());
self.env_vars.insert(key,value);
}
}
pub fn remove_variable(&mut self, key: &str) -> Option<String> {
self.variables.remove(key)
}
// Getters and Setters for `aliases`
pub fn get_alias(&self, key: &str) -> Option<&String> {
if self.flags.contains(EnvFlags::NO_ALIAS) {
return None
}
self.aliases.get(key)
}
pub fn set_alias(&mut self, key: String, mut value: String) -> Result<(), String> {
if self.get_function(key.as_str()).is_some() {
return Err(format!("The name `{}` is already being used as a function",key))
}
if value.starts_with('"') && value.ends_with('"') {
value = value.strip_prefix('"').unwrap().into();
value = value.strip_suffix('"').unwrap().into();
}
self.aliases.insert(key, value);
Ok(())
}
pub fn remove_alias(&mut self, key: &str) -> Option<String> {
self.aliases.remove(key)
}
// Getters and Setters for `functions`
pub fn get_function(&self, name: &str) -> Option<Box<Node>> {
self.functions.get(name).cloned()
}
pub fn set_function(&mut self, name: String, body: Box<Node>) -> Result<(),ShellError> {
if self.get_alias(name.as_str()).is_some() {
return Err(ShellError::from_parse(format!("The name `{}` is already being used as an alias",name).as_str(), body.span()))
}
self.functions.insert(name, body);
Ok(())
}
pub fn remove_function(&mut self, name: &str) -> Option<Box<Node>> {
self.functions.remove(name)
}
// Getters and Setters for `parameters`
pub fn get_parameter(&self, key: &str) -> Option<&String> {
if key == "*" {
// Return the un-split parameter string
return self.parameters.get("@")
}
self.parameters.get(key)
}
pub fn set_parameter(&mut self, key: String, value: String) {
if key.chars().next().unwrap().is_ascii_digit() {
let mut pos_params = self.parameters.remove("@").unwrap_or_default();
if pos_params.is_empty() {
pos_params = value.clone();
} else {
pos_params = format!("{} {}",pos_params,value);
}
self.parameters.insert("@".into(),pos_params.clone());
let num_params = pos_params.split(' ').count();
self.parameters.insert("#".into(),num_params.to_string());
}
self.parameters.insert(key, value);
}
pub fn clear_pos_parameters(&mut self) {
let mut index = 1;
while let Some(_value) = self.get_parameter(index.to_string().as_str()) {
self.parameters.remove(index.to_string().as_str());
index += 1;
}
}
// Utility method to clear the environment
pub fn clear(&mut self) {
self.variables.clear();
self.aliases.clear();
self.functions.clear();
self.parameters.clear();
}
}
fn init_shopts() -> HashMap<String,usize> {
let mut shopts = HashMap::new();
shopts.insert("dotglob".into(),0);
shopts.insert("trunc_prompt_path".into(),4);
shopts.insert("int_comments".into(),1);
shopts.insert("autocd".into(),1);
shopts.insert("hist_ignore_dupes".into(),1);
shopts.insert("max_hist".into(),1000);
shopts.insert("edit_mode".into(),1);
shopts.insert("comp_limit".into(),100);
shopts.insert("auto_hist".into(),1);
shopts.insert("prompt_highlight".into(),1);
shopts.insert("tab_stop".into(),4);
shopts.insert("bell_style".into(),1);
shopts
}

View File

@@ -0,0 +1,9 @@
#!/home/pagedmov/Coding/projects/rust/rsh/target/debug/rsh
case var in
var) echo found it
;;
bar) echo not here
;;
car) echo not here either
;;
esac

View File

@@ -0,0 +1,4 @@
#!/home/pagedmov/Coding/projects/rust/rsh/target/debug/rsh
ls
echo "Hello, World!"
pwd

View File

@@ -0,0 +1,4 @@
#!/home/pagedmov/Coding/projects/rust/rsh/target/debug/rsh
for i in 1 2 3; do
echo $i;
done

View File

@@ -0,0 +1,12 @@
#!/home/pagedmov/Coding/projects/rust/rsh/target/debug/rsh
if [ 1 -eq 1 ]; then
echo true
else
echo false
fi
if [ 1 -ne 1 ]; then
echo true
else
echo false
fi

View File

@@ -0,0 +1,6 @@
#!/home/pagedmov/Coding/projects/rust/rsh/target/debug/rsh
var=1
until [ $var -eq 0 ]; do
var=0;
echo looped
done

View File

@@ -0,0 +1,3 @@
#!/home/pagedmov/Coding/projects/rust/rsh/target/debug/rsh
var="value"
echo $var

View File

@@ -0,0 +1,6 @@
#!/home/pagedmov/Coding/projects/rust/rsh/target/debug/rsh
var=1
while [ $var -eq 1 ]; do
var=0;
echo looped;
done

View File

@@ -0,0 +1,3 @@
#!/home/pagedmov/Coding/projects/rust/rsh/target/debug/rsh
true && echo it works
false && echo it doesnt work

View File

@@ -0,0 +1,3 @@
#!/home/pagedmov/Coding/projects/rust/rsh/target/debug/rsh
var=""
echo $var

View File

@@ -0,0 +1,2 @@
#!/home/pagedmov/Coding/projects/rust/rsh/target/debug/rsh
echo hello world | grep w

View File

@@ -0,0 +1,2 @@
#!/home/pagedmov/Coding/projects/rust/rsh/target/debug/rsh
for var in 1 2 3; do case var in;1) if until until until echo; do echo; done; do if echo; then echo; elif echo; then if echo; then echo; elif echo; then echo; fi; fi; done; do for i in 1 2 3; do echo; done; done; then until if until echo; do echo; done; then if echo; then echo; elif echo; then echo; fi; elif for i in 1 2 3; do echo; done; then until echo; do echo; done; fi; do if echo; then echo; elif echo; then echo; fi; done; fi;;2) until echo; do echo; done;;3) for i in 1 2 3; do echo; done ;;esac; done

View File

@@ -45,7 +45,7 @@ pkgs.writeShellApplication {
running=true running=true
declare -A commands=( declare -A commands=(
["Change Wallpaper"]="moveonscreen --center && if chpaper; then running=false; else moveonscreen; fi" ["Change Wallpaper"]="hyprctl dispatch resizeactive 20% 50% && moveonscreen --center && if chpaper; then running=false; else moveonscreen; fi"
["Change System Color Scheme"]="hyprctl dispatch resizeactive 10% 80% && moveonscreen --center && if chscheme; then running=false; else hyprctl dispatch resizeactive exact 40% 25% && moveonscreen; fi" ["Change System Color Scheme"]="hyprctl dispatch resizeactive 10% 80% && moveonscreen --center && if chscheme; then running=false; else hyprctl dispatch resizeactive exact 40% 25% && moveonscreen; fi"
["Open System Monitor"]="btop_cmd" ["Open System Monitor"]="btop_cmd"
["Open Volume Controls"]="hyprctl dispatch resizeactive 10% 80% && moveonscreen --center && alsamixer && hyprctl dispatch resizeactive exact 40% 25% && moveonscreen" ["Open Volume Controls"]="hyprctl dispatch resizeactive 10% 80% && moveonscreen --center && alsamixer && hyprctl dispatch resizeactive exact 40% 25% && moveonscreen"
@@ -64,24 +64,7 @@ pkgs.writeShellApplication {
# Use fzf to select a command with preview # Use fzf to select a command with preview
while $running; do while $running; do
selected_command=$(printf "%s\n" "''${ordered_commands[@]}" | fzf --preview=" selected_command=$(printf "%s\n" "''${ordered_commands[@]}" | fzf --prompt="> ")
cleaned_key=\$(echo {} | tr -d \"'\"); \
echo \"Cleaned key: \$cleaned_key\"; \
declare -A descriptions=(
[\"Change Wallpaper\"]=\"Choose a wallpaper to switch to from the assets/wallpapers folder in the system flake directory. Requires rebuilding the system and restarting hyprpaper.\"
[\"Change System Color Scheme\"]=\"Changes the base16 color scheme used by stylix to color system applications.\"
[\"Open System Monitor\"]=\"Opens a btop window.\"
[\"Open Volume Controls\"]=\"Opens alsamixer.\"
[\"Open Keyring\"]=\"Opens a fuzzy finder with all of the paths held in ~/.password-store. Selecting one uses pass to copy that password to the clipboard. Password is cleared from clipboard after 45 seconds, and isn't saved to clipboard history.\"
[\"View Clipboard History\"]=\"Opens clipboard history. Selecting an item copies it to the clipboard.\"
); \
if [[ -v descriptions[\$cleaned_key] ]]; then \
clear; \
echo \''${descriptions[\$cleaned_key]} | fmt -w 28; \
else \
clear; \
echo \"No description available\"; \
fi" --prompt="> ")
#Execute the selected command if selection is not empty #Execute the selected command if selection is not empty