swiss-army-knife (empty) → 1.1.0.4
raw patch · 19 files changed
Files
- CHANGELOG.md +51/−0
- CHANGELOG.org +45/−0
- LICENSE +28/−0
- README.org +79/−0
- Setup.hs +2/−0
- app/SwissArmyKnife.hs +25/−0
- lib/CommandLine.hs +178/−0
- lib/Knives.hs +23/−0
- lib/Knives/ExtIP.hs +89/−0
- lib/Knives/Kernel.hs +143/−0
- lib/Knives/NetMan.hs +31/−0
- lib/Knives/Sleep.hs +17/−0
- lib/Knives/SysNet.hs +46/−0
- lib/Knives/Version.hs +12/−0
- lib/Knives/WireGuard.hs +108/−0
- lib/Knives/ZfsCheck.hs +44/−0
- lib/Utils.hs +32/−0
- swiss-army-knife.cabal +91/−0
- test/Spec.hs +2/−0
+ CHANGELOG.md view
@@ -0,0 +1,51 @@+# 1.1.0.4 - 2026-07-01++- Adjusted cabal version to be lower for better coverage.+- GHC2021 for better coverage++## Added++- Cross-distro `sak kernel` command+ - Now works on Arch, Debian, Ubuntu, Linux Mint, Pop!<sub>OS</sub>, Fedora, RHEL, CentOS, Rocky Linux, and AlmaLinux+ - Detects running kernel via `uname -r`+ - Shows installed kernels using the native package manager (`pacman`, `dpkg-query`, or `rpm`)+ - Graceful handling when optional kernel packages (e.g. `linux-lts`) are not installed++## Changed++- `Knives.Kernel` module is now fully distribution-agnostic+- Cleaner output formatting with proper newlines+- More robust command execution (no crashes on missing packages)++## Fixed++- Trailing newlines from `readProcess`+- Ambiguous exception type error on GHC 9.14++# 1.0.0.1 and earlier++## Versions++- 1.1.0.0 - Hackage release, renamed to swiss-army-knife (dropping the "-hs" from the name. AUR will pull from Hackage instead. of GitHub.++- 1.0.0.1 - enable and disable daemons, wg -l list has an extra field.++ - Changelog.org removed. Basically a duplication of Versions here.+ - version 1.0.0.0 was stillborn. Installation issues fixed++- 0.1.0.4 - fix for install issues with 0.1.0.3++- 0.1.0.3 - 2 new feature, internally invokes sudo, adopted "standard" for 4 number versioning X.Y.Z.W:++ | \# | Description |+ |-----|-----------------------------------------------|+ | X | Major version (breaking changes) |+ | Y | Minor version (new features) |+ | Z | Patch version (bug fixes) |+ | W | Build or revision number (specific iteration) |++ So from now on, this will be applied.++- 0.0.1.1 - new feature added++- 0.0.0.2 - bug fixes
+ CHANGELOG.org view
@@ -0,0 +1,45 @@+#+TITLE: Changelog for swiss-army-knife+#+AUTHOR: Fred Mitchell+#+EMAIL: fred.mitchell@atomlogik.de++* 1.1.0.4 - 2026-07-01+ + Adjusted cabal version to be lower for better coverage.+ + GHC2021 for better coverage+** Added+- Cross-distro =sak kernel= command+ - Now works on Arch, Debian, Ubuntu, Linux Mint, Pop!_OS, Fedora, RHEL, CentOS, Rocky Linux, and AlmaLinux+ - Detects running kernel via =uname -r=+ - Shows installed kernels using the native package manager (=pacman=, =dpkg-query=, or =rpm=)+ - Graceful handling when optional kernel packages (e.g. =linux-lts=) are not installed++** Changed+- =Knives.Kernel= module is now fully distribution-agnostic+- Cleaner output formatting with proper newlines+- More robust command execution (no crashes on missing packages)++** Fixed+- Trailing newlines from =readProcess=+- Ambiguous exception type error on GHC 9.14++* 1.0.0.1 and earlier+** Versions+ + 1.1.0.0 - Hackage release, renamed to swiss-army-knife (dropping the+ "-hs" from the name. AUR will pull from Hackage instead.+ of GitHub.+ + 1.0.0.1 - enable and disable daemons, wg -l list has an extra field.+ - Changelog.org removed. Basically a duplication of Versions here.+ - version 1.0.0.0 was stillborn. Installation issues fixed+ + 0.1.0.4 - fix for install issues with 0.1.0.3+ + 0.1.0.3 - 2 new feature, internally invokes sudo,+ adopted "standard" for 4 number versioning X.Y.Z.W:++ | # | Description |+ |---+-----------------------------------------------|+ | X | Major version (breaking changes) |+ | Y | Minor version (new features) |+ | Z | Patch version (bug fixes) |+ | W | Build or revision number (specific iteration) |++ So from now on, this will be applied.+ + 0.0.1.1 - new feature added+ + 0.0.0.2 - bug fixes
+ LICENSE view
@@ -0,0 +1,28 @@+Copyright Fred Mitchell (c) 2024++All rights reserved.++Permission is hereby granted, free of charge, to any+person obtaining a copy of this software and associated+documentation files (the "Software"), to deal in the+Software without restriction, including without+limitation the rights to use, copy, modify, merge,+publish, distribute, sublicense, and/or sell copies of+the Software, and to permit persons to whom the+Software is furnished to do so, subject to the+following conditions:++The above copyright notice and this permission notice+shall be included in all copies or substantial portions+of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR+ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR+OTHER DEALINGS IN THE SOFTWARE.+
+ README.org view
@@ -0,0 +1,79 @@+* swiss-army-knife :TOC_7_gh:+ - [[#created-by][Created By]]+ - [[#introduction][Introduction]]+ - [[#documentation][Documentation]]+ - [[#sak---help][sak --help]]+ - [[#sak-extip][sak extip]]+ - [[#sak-kernel][sak kernel]]+ - [[#sak-sleep][sak sleep]]+ - [[#sak-zfscheck][sak zfscheck]]+ - [[#sak-wg][sak wg]]+ - [[#sak-nm][sak nm]]+ - [[#sak-sn][sak sn]]+ - [[#examples][Examples]]++** Created By+ Fred Mitchell & Atomlogik+ +** Introduction+ Swiss-Army-Knife is a collection of useful+ tools that you can do quickly.+ + This is now in Hackage and the AUR. So in theory,+ it can run "anywhere", though I have not had the time to test this,+ so I am open to comments and pull requests. In particular, it is + gauranteed to run on Arch, and I do have some support for other+ distros, and that will grow. But I have not had the time to fully+ test other distros.++ So some knives will run "anywhere", but some that call out to+ the system for system-specific information may have issues.+ As I find out about them, I will fix them as quickly as I can.++** Documentation+ These are the current "knives" implemented. More to come. Open+ to suggestions.+ + Note that you no longer have to prepend "sudo" to do system-level+ operations.+*** sak --help+ Will list all the current commands.+*** sak extip+ Will tell you what your external IP is for your connection,+ either in IPv4 or IPv6 Useful if you use a VPN+*** sak kernel+ Will tell you the version of the installed kernel as well+ as the running version. Support is in for Arch, Debian, and+ RedHat, but only Arch has been tested.+*** sak sleep+ Will put your system in standby mode.+*** sak zfscheck+ If you use the zfs from archzfs, sometimes it + does not keep up with the latest kernel release.+ Check to see the latest kernel zfs supports before+ you do a system upgrade. Other distros will probably+ not have the zfs sync issues that Arch does.+*** sak wg+ If you use WireGuard for your VPN, manage it from+ here. List your installed WireGuard options,+ and swap among them. you can start, stop, and list your+ WireGuard VPNs.++ Now supports enabling and disabling of the underlying daemon.+*** sak nm+ Manage NetworkManager+ Now supports enabling and disabling of the underlying daemon.++*** sak sn+ Manage systemd-networkd+ Now supports enabling and disabling of the underlying daemon.++** Examples+ | Command | Description |+ |-----------+--------------------------------------------------------------------------------------------------------------------|+ | sak extip | Will list your external IP and location information |+ | sak wg -l | (If you are using wg_quick) will list all of your available WG vpns, as well as their active and enabled statuses. |+ | sak wg -d | Will deactivate (but not disable) all WireGuard VPNs. |+ | sak wg -D | Will disable (but not deactivate) all WireGuard VPNs. |+ | sak sn -A | Will enable and start systemd-networkd. |+ | sak nm -D | Will disable and stop NetworkManager. |+ |-----------+--------------------------------------------------------------------------------------------------------------------|
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/SwissArmyKnife.hs view
@@ -0,0 +1,25 @@+-- main for Swiss Army Knife, cli.++module Main where++import Options.Applicative+import Data.Semigroup ((<>))+import CommandLine+import Knives++main :: IO ()+main = do+ (globalOpts, cmd) <- execParser $ info (opts <**> helper)+ ( fullDesc+ <> progDesc "Many useful utilities, such as getting your external IP address, installed kernel, controlling your WireGuard, etc."+ <> header "Swiss Army Knife -- Many useful functions for the hacker in all of us." )+ + case cmd of+ ExtIP extipOpts -> knifeExtIP extipOpts+ Kernel kernelOpts -> knifeKernel kernelOpts+ Sleep sleepOpts -> knifeSleep sleepOpts + Version versionOpts -> knifeVersion versionOpts + ZfsCheck zfsOpts -> knifeZfsCheck zfsOpts+ WireGuard wgOpts -> knifeWireGuard wgOpts+ NetMan nmOpts -> knifeNetMan nmOpts+ SysNet snOpts -> knifeSysNet snOpts
+ lib/CommandLine.hs view
@@ -0,0 +1,178 @@+{-# LANGUAGE OverloadedStrings #-}++module CommandLine where++import Options.Applicative++-- Define data types for our commands and options+data Command+ = ExtIP ExtIPOptions+ | Version VersionOptions+ | Kernel KernelOptions+ | Sleep SleepOptions+ | ZfsCheck ZfsCheckOptions+ | WireGuard WireGuardOptions+ | NetMan NetManOptions+ | SysNet SysNetOptions+ deriving Show ++data ExtIPOptions = ExtIPOptions+ { nolocation :: Bool+ , ipv6 :: Bool+ } deriving Show++data KernelOptions = KernelOptions+ { justVersion :: Bool } deriving Show++data SleepOptions = SleepOptions+ { secondsToSleep :: Maybe Int } deriving Show++data VersionOptions = VersionOptions Bool deriving Show++data ZfsCheckOptions = ZfsCheckOptions+ { full :: Bool } deriving Show++data WireGuardOptions = WireGuardOptions+ { listWGs :: Bool+ , activateWG :: Maybe String+ , enableWG :: Maybe String+ , deactivateWG :: Bool+ , disableWG :: Bool+ , reactivateWG :: Bool+ } deriving Show++data NetManOptions = NetManOptions+ { activateNM :: Bool+ , enableNM :: Bool+ , deactivateNM :: Bool+ , disableNM :: Bool+ , reactivateNM :: Bool+ } deriving Show++data SysNetOptions = SysNetOptions+ { activateSN :: Bool+ , enableSN :: Bool+ , deactivateSN :: Bool+ , disableSN :: Bool+ , reactivateSN :: Bool+ } deriving Show++data GlobalOptions = GlobalOptions+ { verbose :: Bool } deriving Show++globalOptionsParser :: Parser GlobalOptions+globalOptionsParser = GlobalOptions+ <$> switch ( long "verbose"+ <> short 'v'+ <> help "Enable verbose mode" )++extipOptionsParser :: Parser ExtIPOptions+extipOptionsParser = ExtIPOptions+ <$> switch ( long "nolocation"+ <> short 'n'+ <> help "Don't display location information" )+ <*> switch ( long "ipv6"+ <> short 'i'+ <> help "Display the IPv6 address" )++kernelOptionsParser :: Parser KernelOptions+kernelOptionsParser = KernelOptions+ <$> switch ( long "justversion"+ <> short 'j'+ <> help "Just display the numerical version number, no adjoining text." )++-- version has no options+versionOptionsParser :: Parser VersionOptions+versionOptionsParser = VersionOptions+ <$> switch ( long "noop" )+ +sleepOptionsParser :: Parser SleepOptions+sleepOptionsParser = SleepOptions+ <$> optional (option auto ( long "wait"+ <> short 'w'+ <> metavar "SECONDS"+ <> help "Seconds to wait before going to sleep"))++zfscheckOptionsParser :: Parser ZfsCheckOptions+zfscheckOptionsParser = ZfsCheckOptions+ <$> switch+ ( long "full"+ <> short 'f'+ <> help "Check all kernel versions" )++wgOptionsParser :: Parser WireGuardOptions+wgOptionsParser = WireGuardOptions+ <$> switch ( long "list"+ <> short 'l'+ <> help "List all installed WireGuardG VPNs")+ <*> optional (strOption ( long "activate"+ <> short 'a'+ <> metavar "WGVPN"+ <> help "WireGuard VPN to activate"))+ <*> optional (strOption ( long "enable"+ <> short 'A'+ <> metavar "WGVPN"+ <> help "WireGuard VPN to enable and activate"))+ <*> switch ( long "deactivate"+ <> short 'd'+ <> help "Deactivate (but not disable) all WireGuard VPNs")+ <*> switch ( long "disable"+ <> short 'D'+ <> help "Disable (but not deactivate) all WireGuard VPNs")+ <*> switch ( long "reactivate"+ <> short 'r'+ <> help "Reactivate currently active WireGuardG VPNs")++nmOptionsParser :: Parser NetManOptions+nmOptionsParser = NetManOptions+ <$> switch ( long "activate"+ <> short 'a'+ <> help "Activate (start) NetworkManager")+ <*> switch ( long "enable"+ <> short 'A'+ <> help "Enable and Activate (start) NetworkManager")+ <*> switch ( long "deactivate"+ <> short 'd'+ <> help "Deactivate (stop) NetworkManager")+ <*> switch ( long "disable"+ <> short 'D'+ <> help "Disable and Deactivate (stop) NetworkManager")+ <*> switch ( long "reactivate"+ <> short 'r'+ <> help "Reactivate (restart) NetworkManager")++snOptionsParser :: Parser SysNetOptions+snOptionsParser = SysNetOptions+ <$> switch ( long "activate"+ <> short 'a'+ <> help "Activate (start) systemd-networkd socket and service")+ <*> switch ( long "enable"+ <> short 'A'+ <> help "Enable and Activate (start) systemd-networkd socket and service")+ <*> switch ( long "deactivate"+ <> short 'd'+ <> help "Deactivate (stop) systemd-networkd socket and service")+ <*> switch ( long "disable"+ <> short 'D'+ <> help "Disable and Deactivate (stop) systemd-networkd socket and service")+ <*> switch ( long "reactivate"+ <> short 'r'+ <> help "Reactivate (restart) systemd-networkd socket and service")+++-- Combine the subcommand parsers+commandParser :: Parser Command+commandParser = subparser+ ( command "extip" (info (ExtIP <$> extipOptionsParser) (progDesc "Display external IP address"))+ <> command "kernel" (info (Kernel <$> kernelOptionsParser) (progDesc "Display kernel information, both installed and currently running"))+ <> command "sleep" (info (Sleep <$> sleepOptionsParser) (progDesc "Put the machine to sleep"))+ <> command "version" (info (Version <$> versionOptionsParser) (progDesc "Version of Swiss Army Knife"))+ <> command "zfscheck" (info (ZfsCheck <$> zfscheckOptionsParser) (progDesc "Check zfs availibility for the current kernel"))+ <> command "wg" (info (WireGuard <$> wgOptionsParser) (progDesc "Manage WireGuard VPNs"))+ <> command "nm" (info (NetMan <$> nmOptionsParser) (progDesc "Manage NetworkManager"))+ <> command "sn" (info (SysNet <$> snOptionsParser) (progDesc "Manage systemd-networkd"))+ )++-- Combine global options with the command parser+opts :: Parser (GlobalOptions, Command)+opts = (,) <$> globalOptionsParser <*> commandParser
+ lib/Knives.hs view
@@ -0,0 +1,23 @@+{-# LANGUAGE OverloadedStrings #-}++module Knives+ ( knifeKernel+ , knifeExtIP+ , knifeSleep+ , knifeVersion+ , knifeZfsCheck+ , knifeWireGuard+ , knifeNetMan+ , knifeSysNet+ ) where+++import Knives.ExtIP+import Knives.Kernel+import Knives.Sleep+import Knives.Version+import Knives.WireGuard+import Knives.ZfsCheck+import Knives.NetMan+import Knives.SysNet+
+ lib/Knives/ExtIP.hs view
@@ -0,0 +1,89 @@+{-# LANGUAGE OverloadedStrings #-}++module Knives.ExtIP where++import CommandLine+import Network.HTTP.Simple+import Data.Aeson+import Data.Bool (bool)+import Control.Monad ((>=>))++data RExtIP = RExtIP+ { ip :: String } deriving Show++instance FromJSON RExtIP where+ parseJSON = withObject "RExtIP" $ \v -> RExtIP+ <$> v .: "ip"++data RLoc = RLoc+ {status :: String+ , country :: String+ , countryCode:: String+ , region :: String+ , regionName :: String+ , city :: String+ , zip :: String+ , lat :: Float+ , lon :: Float+ , timezone :: String+ , isp :: String+ , org :: String+ , as :: String+ , query :: String+ } deriving Show++instance FromJSON RLoc where+ parseJSON = withObject "RLoc" $ \v -> RLoc+ <$> v .: "status"+ <*> v .: "country"+ <*> v .: "countryCode"+ <*> v .: "region"+ <*> v .: "regionName"+ <*> v .: "city"+ <*> v .: "zip"+ <*> v .: "lat"+ <*> v .: "lon"+ <*> v .: "timezone"+ <*> v .: "isp"+ <*> v .: "org"+ <*> v .: "as"+ <*> v .: "query"+ +knifeExtIP :: ExtIPOptions -> IO ()+knifeExtIP optsE = do+ res <- (parseRequest >=> httpLBS) $ bool url_ipv4 url_ipv6 $ ipv6 optsE+ putStrLn $ ipaddr res+ if (not $ nolocation optsE)+ then do+ loc <- location+ putStrLn $ loc+ else return ()++ where+ url_ipv4 = "https://ipv4.jsonip.com"+ url_ipv6 = "https://ipv6.jsonip.com"+ url_geo = "http://ip-api.com/json/"+ ipaddr res = do+ let ipresult = eitherDecode $ getResponseBody res :: Either String RExtIP+ case ipresult of+ Left err -> do+ "Failed to obtain IP: " ++ err+ Right extIP -> do+ ip extIP++ location :: IO String+ location = do+ request <- parseRequest url_geo+ res <- httpLBS request+ let locres = eitherDecode $ getResponseBody res :: Either String RLoc+ case locres of+ Left err -> do+ return $ "Failed to obtain location: " ++ err+ Right loc -> do+ let str_result = "Location: "+ ++ city loc ++ ", "+ ++ country loc+ ++ " (" ++ countryCode loc ++ ")\n"+ ++ "Timezone: " ++ timezone loc ++ "\n"+ + return str_result
+ lib/Knives/Kernel.hs view
@@ -0,0 +1,143 @@+module Knives.Kernel+ ( knifeKernel+ , KernelOptions(..)+ ) where++import CommandLine (KernelOptions(..))+import System.Process (readProcess)+import Control.Exception (try, IOException)+import Data.Char (isSpace)+import Data.List (dropWhileEnd, isInfixOf, isPrefixOf)+import System.Directory (doesFileExist)+import Data.Maybe (catMaybes)++-- | Trim leading and trailing whitespace/newlines+trim :: String -> String+trim = dropWhileEnd isSpace . dropWhile isSpace++-- | Safely run a command. Returns Nothing if it fails or command not found.+tryReadProcess :: String -> [String] -> IO (Maybe String)+tryReadProcess cmd args = do+ result <- try @IOException (readProcess cmd args "")+ pure $ either (const Nothing) (Just . trim) result++-- | Detect Debian-based systems (Debian, Ubuntu, Mint, Pop!_OS, Kali, etc.)+isDebianBased :: IO Bool+isDebianBased = do+ hasDebian <- doesFileExist "/etc/debian_version"+ if hasDebian+ then pure True+ else checkOsRelease+ where+ checkOsRelease = do+ hasOs <- doesFileExist "/etc/os-release"+ if not hasOs then pure False else do+ content <- readFile "/etc/os-release"+ let debianLike = ["debian", "ubuntu", "linuxmint", "pop", "kali", "elementary", "zorin"]+ pure $ any (\idName ->+ ("ID=" ++ idName) `isInfixOf` content ||+ ("ID_LIKE=" ++ idName) `isInfixOf` content+ ) debianLike++-- | Detect Red Hat family (RHEL, CentOS, Fedora, Rocky, AlmaLinux, Oracle, etc.)+isRedHatBased :: IO Bool+isRedHatBased = do+ hasRedhat <- doesFileExist "/etc/redhat-release"+ if hasRedhat+ then pure True+ else checkOsRelease+ where+ checkOsRelease = do+ hasOs <- doesFileExist "/etc/os-release"+ if not hasOs then pure False else do+ content <- readFile "/etc/os-release"+ let redhatLike = ["rhel", "centos", "fedora", "rocky", "almalinux", "ol", "scientific"]+ pure $ any (\idName ->+ ("ID=" ++ idName) `isInfixOf` content ||+ ("ID_LIKE=" ++ idName) `isInfixOf` content+ ) redhatLike++-- | Get the currently running kernel (works on all Linux distros)+getRunningKernel :: IO String+getRunningKernel = do+ mVer <- tryReadProcess "uname" ["-r"]+ pure $ maybe "unknown" id mVer++-- | Get installed kernel packages (distro-aware)+getInstalledKernels :: IO [String]+getInstalledKernels = do+ debian <- isDebianBased+ if debian+ then getDebianKernels+ else do+ redhat <- isRedHatBased+ if redhat+ then getRedHatKernels+ else getArchKernels++-- | Debian / Ubuntu / Mint style+getDebianKernels :: IO [String]+getDebianKernels = do+ mOut <- tryReadProcess "dpkg-query"+ ["-W", "-f=${Package} ${Version}\n", "linux-image-*"]+ case mOut of+ Just out | not (null out) ->+ pure $ filter (not . null) $ lines out+ _ -> do+ -- Fallback to dpkg -l+ mOut2 <- tryReadProcess "dpkg" ["-l"]+ case mOut2 of+ Just out2 ->+ pure $ take 8+ $ map (unwords . take 4 . words)+ $ filter ("linux-image" `isInfixOf`) (lines out2)+ Nothing -> pure []++-- | Red Hat family (RHEL, CentOS, Fedora, Rocky, AlmaLinux, etc.)+getRedHatKernels :: IO [String]+getRedHatKernels = do+ mOut <- tryReadProcess "rpm"+ [ "-qa"+ , "--qf"+ , "%{NAME}-%{VERSION}-%{RELEASE}\n"+ , "kernel*"+ ]+ case mOut of+ Just out | not (null (trim out)) ->+ pure $ take 8+ $ filter (\pkg ->+ "kernel" `isPrefixOf` pkg &&+ not ("headers" `isInfixOf` pkg) &&+ not ("devel" `isInfixOf` pkg) &&+ not ("doc" `isInfixOf` pkg) &&+ not ("tools" `isInfixOf` pkg)+ )+ $ lines (trim out)+ _ -> pure []++-- | Arch Linux style+getArchKernels :: IO [String]+getArchKernels = do+ mLinux <- tryReadProcess "pacman" ["-Q", "linux"]+ mLts <- tryReadProcess "pacman" ["-Q", "linux-lts"]+ mZen <- tryReadProcess "pacman" ["-Q", "linux-zen"]+ mHard <- tryReadProcess "pacman" ["-Q", "linux-hardened"]+ pure $ catMaybes [mLinux, mLts, mZen, mHard]++-- | Main entry point for the `sak kernel` command+knifeKernel :: KernelOptions -> IO ()+knifeKernel optsK = do+ running <- getRunningKernel++ if justVersion optsK+ then putStrLn running+ else do+ installed <- getInstalledKernels++ putStrLn $ "Running kernel: " ++ running++ if null installed+ then putStrLn "Installed kernels: (could not determine on this distribution)"+ else do+ putStrLn "Installed kernels:"+ mapM_ (putStrLn . (" " ++)) installed
+ lib/Knives/NetMan.hs view
@@ -0,0 +1,31 @@+module Knives.NetMan where++import Utils+import CommandLine+import Control.Monad (when)++knifeNetMan :: NetManOptions -> IO ()+knifeNetMan NetManOptions { activateNM+ , enableNM+ , deactivateNM+ , disableNM+ , reactivateNM}+ | isExclusiveOr [ activateNM+ , enableNM+ , deactivateNM+ , disableNM+ , reactivateNM] = do+ when activateNM activate+ when enableNM enable+ when deactivateNM deactivate+ when disableNM disable+ when reactivateNM reactivate+ return ()+ | otherwise = putStrLn("You must specify one and only one option for NetMan.")+ where+ service = "NetworkManager.service"+ activate = systemctl_ ["start", service]+ enable = systemctl_ ["enable", "--now", service]+ deactivate = systemctl_ ["stop", service]+ disable = systemctl_ ["disable", "--now", service]+ reactivate = systemctl_ ["restart", service]
+ lib/Knives/Sleep.hs view
@@ -0,0 +1,17 @@+{-# LANGUAGE OverloadedStrings #-}++module Knives.Sleep where++import System.Process+import CommandLine++knifeSleep :: SleepOptions -> IO ()+knifeSleep optsS = do+ putStrLn $ "Put the machine to sleep." ++ show (secondsToSleep optsS)+ case (secondsToSleep optsS) of+ Just secs -> do putStrLn $ "sleep in " ++ show secs ++ " seconds."+ _n <- readProcess "sleep" [show secs] ""+ return ()+ Nothing -> putStrLn "sleep immediatly"+ _nn <- readProcess "systemctl" ["suspend", "-i"] ""+ return ()
+ lib/Knives/SysNet.hs view
@@ -0,0 +1,46 @@+module Knives.SysNet where++import Utils+import CommandLine+import Control.Monad (when)++knifeSysNet :: SysNetOptions -> IO ()+knifeSysNet SysNetOptions { activateSN+ , enableSN+ , deactivateSN+ , disableSN+ , reactivateSN}+ | isExclusiveOr [ activateSN+ , enableSN+ , deactivateSN+ , disableSN+ , reactivateSN] = do+ when activateSN activate+ when enableSN enable+ when deactivateSN deactivate+ when disableSN disable+ when reactivateSN reactivate+ | otherwise = putStrLn("You must specify one and only one option for SysNet.")+ where+ socket = "systemd-networkd.socket"+ service = "systemd-networkd.service"+ + activate = do+ systemctl_ ["start", socket]+ systemctl_ ["start", service]++ enable = do+ systemctl_ ["start", socket]+ systemctl_ ["enable", "--now", service]++ deactivate = do+ systemctl_ ["stop", socket]+ systemctl_ ["stop", service]+ + disable = do+ systemctl_ ["stop", socket]+ systemctl_ ["disable", "--now", service]+ + reactivate = do+ systemctl_ ["restart", socket]+ systemctl_ ["restart", service]
+ lib/Knives/Version.hs view
@@ -0,0 +1,12 @@+{-# LANGUAGE OverloadedStrings #-}++module Knives.Version where++import CommandLine+import qualified Paths_swiss_army_knife as SAK+import Data.Version (showVersion)++knifeVersion :: VersionOptions -> IO ()+knifeVersion _ = do+ putStrLn $ "Swiss Army Knife version: " ++ showVersion SAK.version+ return ()
+ lib/Knives/WireGuard.hs view
@@ -0,0 +1,108 @@+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -Wno-unused-top-binds #-}++module Knives.WireGuard where++import Utils (systemctl)+import CommandLine+-- import System.IO+-- import System.Process+import System.Directory (getDirectoryContents)+import System.FilePath (dropExtension, takeExtension)+import Control.Monad (when)+import Data.List++knifeWireGuard :: WireGuardOptions -> IO ()+knifeWireGuard WireGuardOptions { listWGs+ , activateWG+ , enableWG+ , deactivateWG+ , disableWG+ , reactivateWG}+ | checkMutualExclusivity = do+ wgs <- wgList+ ss <- wgStatus wgs+ when listWGs $ putStrLn $ formatStatus ss+ case (activateWG , enableWG , deactivateWG , disableWG , reactivateWG) of+ (Just vpn , Nothing , False , False , False) -> activate vpn+ (Nothing , Just vpn , False , False , False) -> enable vpn+ (Nothing , Nothing , True , False , False) -> deactivate+ (Nothing , Nothing , False , True , False) -> disable+ (Nothing , Nothing , False , False , True ) -> reactivate+ _ -> return ()+ | otherwise = putStrLn "Options aside from --list or -l are mutually exclusive"+ where+ wgDir = "/etc/wireguard/"++ wgList :: IO [String]+ wgList = do+ rawlist <- getDirectoryContents wgDir+ let wgconf = filter (\f -> takeExtension f == ".conf") rawlist+ let wgs = map (\f -> dropExtension f) wgconf+ return wgs+ + wgStatus :: [String] -> IO [(String, String, String, Bool, Bool)]+ wgStatus wgs = do+ let quick = map (\w -> vpn2wg w) wgs+ let cmd_a = ["is-active"] <> quick+ let cmd_e = ["is-enabled"] <> quick+ stats_a <- systemctl cmd_a+ stats_e <- systemctl cmd_e+ return $ [(wg+ , sact+ , sena+ , sact == "active"+ , sena == "enabled") | (wg, sact, sena) <- zip3 wgs stats_a stats_e ]++ formatStatus :: [(String, String, String, Bool, Bool)] -> String+ formatStatus wss = intercalate "\n" [w <> ": " <> sa <> " " <> se |(w, sa, se, _, _) <- wss]++ checkMutualExclusivity = check [ activateWG /= Nothing+ , enableWG /= Nothing+ , deactivateWG+ , disableWG+ , reactivateWG]++ check bs = (length $ filter id bs) `elem` [0, 1]+ vpn2wg vpn = "wg-quick@" <> vpn+ + activate :: String -> IO ()+ activate vpn = do+ _ <- systemctl ["start", vpn2wg vpn]+ return ()++ enable :: String -> IO ()+ enable vpn = do+ _ <- systemctl ["enable", "--now", vpn2wg vpn]+ return ()+ + deactivate :: IO ()+ deactivate = do+ awgs <- activeWG+ mapM_ (\w -> systemctl $ ["stop"] <> [vpn2wg w]) awgs+ return ()++ disable :: IO ()+ disable = do+ awgs <- enabledWG+ mapM_ (\w -> systemctl $ ["disable", "--now"] <> [vpn2wg w]) awgs+ return ()+++ reactivate :: IO ()+ reactivate = do+ awgs <- activeWG+ mapM_ (\w -> systemctl $ ["restart"] <> [vpn2wg w]) awgs+ return()++ activeWG :: IO [String]+ activeWG = do+ wgl <- wgList+ wgs <- wgStatus wgl+ return [s | (s, _, _, a, _) <- wgs, a]++ enabledWG :: IO [String]+ enabledWG = do+ wgl <- wgList+ wgs <- wgStatus wgl+ return [s | (s, _, _, _, e) <- wgs, e]
+ lib/Knives/ZfsCheck.hs view
@@ -0,0 +1,44 @@++module Knives.ZfsCheck where++import CommandLine+import Data.List.Split (splitOn)+import Data.Char (isSpace)+import Data.List (isInfixOf)+import qualified Data.ByteString.Lazy as LBS+import qualified Data.ByteString.Lazy.Char8 as LBS8+import System.Process+++knifeZfsCheck :: ZfsCheckOptions -> IO ()+knifeZfsCheck _ = do+ putStrLn $ "Check the version of archzfs with repo kernel version so you can determine if an upgrade is possible."++ let url_zfs = "https://raw.githubusercontent.com/openzfs/zfs/master/META"++ repo_version <- readProcess "pacman" ["-Si", "linux"] "" -- | awk '/^Version/ { print $3 }'" ""+ archzfs_version <- readProcess "curl" ["-sSL", url_zfs] ""+ skernel <- readProcess "uname" ["-r"] ""+ sinstalled <- readProcess "pacman" ["-Q", "linux"] ""++ let repo = splitAndTrim+ $ LBS8.unpack+ $ head [v | v <- LBS8.lines (LBS8.pack repo_version), (LBS8.pack "Version") `LBS.isPrefixOf` v]+ archzfs = splitAndTrim+ $ LBS8.unpack+ $ head [v | v <- LBS8.lines (LBS8.pack archzfs_version), (LBS8.pack "Linux-Maximum:") `LBS.isPrefixOf` v]+ running = trim skernel+ installed = unwords+ $ filter (not . ("linux" `isInfixOf`))+ $ words+ $ trim sinstalled++ putStrLn $ "\n repo: " ++ repo+ ++ "\n zfs: " ++ archzfs+ ++ "\n running: " ++ running+ ++ "\n installed: " ++ installed++ where+ splitAndTrim line = trim $ last $ splitOn ":" line+ trim = f . f+ where f = reverse . dropWhile isSpace
+ lib/Utils.hs view
@@ -0,0 +1,32 @@+module Utils where++import System.IO+import System.Process+-- import System.Directory (getDirectoryContents)+-- import System.FilePath (dropExtension, takeExtension)++-- WARN: Cannot handle inflinite lists.+isExclusiveOr :: [Bool] -> Bool+isExclusiveOr bs = (length $ filter id bs) == 1++-- WARN: Cannot handle inflinite lists.+isExclusiveOrNone :: [Bool] -> Bool+isExclusiveOrNone bs = (length $ filter id bs) `elem` exorno+ where + exorno = [0, 1]++systemctl :: [String] -> IO [String]+systemctl parms = systemG $ "sudo" : "systemctl" : parms + +systemctl_ :: [String] -> IO ()+systemctl_ parms = do+ _ <- systemG $ "sudo" : "systemctl" : parms+ return ()++systemG :: [String] -> IO [String]+systemG [] = return []+systemG (cmd:parms) = do+ (_, Just hout, _, _) <- createProcess (proc cmd parms) { std_out = CreatePipe }+ out <- hGetContents hout+ return $ lines out+
+ swiss-army-knife.cabal view
@@ -0,0 +1,91 @@+cabal-version: 2.4+name: swiss-army-knife+version: 1.1.0.4+synopsis: A collection of small tools to make life a bit easier for the command-line.+category: System+description: Please see the README on GitHub at <https://github.com/flajann2/swiss-army-knife#readme>+homepage: https://github.com/flajann2/swiss-army-knife#readme+bug-reports: https://github.com/flajann2/swiss-army-knife/issues+author: Fred Mitchell+maintainer: fred.mitchell@atomlogik.de+copyright: 2024-2026 Fred Mitchell & Atomlogik+license: MIT+license-file: LICENSE+build-type: Simple+extra-source-files:+ README.org+ CHANGELOG.org+ LICENSE+extra-doc-files:+ README.org+ CHANGELOG.org+ CHANGELOG.md+ +tested-with: GHC == 9.14.1++source-repository head+ type: git+ location: https://github.com/flajann2/swiss-army-knife++library+ exposed-modules:+ CommandLine+ Utils+ Knives+ Knives.Version+ Knives.Sleep+ Knives.ExtIP+ Knives.WireGuard+ Knives.ZfsCheck+ Knives.Kernel+ Knives.SysNet+ Knives.NetMan+ autogen-modules:+ Paths_swiss_army_knife+ other-modules:+ Paths_swiss_army_knife+ hs-source-dirs:+ lib+ ghc-options: -Wall+ build-depends:+ aeson >= 2.1 && < 2.4+ , base >= 4.18 && < 5+ , bytestring >= 0.11 && < 0.13+ , directory >= 1.3.8 && < 1.4+ , filepath >= 1.4.2 && < 1.6+ , http-conduit >= 2.3.8 && < 2.4+ , optparse-applicative >= 0.18 && < 0.20+ , process >= 1.6.18 && < 1.7+ , resolv >= 0.1.2 && < 0.3+ , split >= 0.2.5 && < 0.3+ default-language: GHC2021++executable sak+ main-is: SwissArmyKnife.hs+ autogen-modules:+ Paths_swiss_army_knife+ other-modules:+ Paths_swiss_army_knife+ hs-source-dirs:+ app+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ base >= 4.18 && < 5+ , optparse-applicative >= 0.18 && < 0.20+ , swiss-army-knife+ default-language: GHC2021++test-suite swiss-army-knife-test+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ autogen-modules:+ Paths_swiss_army_knife+ other-modules:+ Paths_swiss_army_knife+ hs-source-dirs:+ test+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ base >= 4.18 && < 5+ , swiss-army-knife+ default-language: GHC2021
+ test/Spec.hs view
@@ -0,0 +1,2 @@+main :: IO ()+main = putStrLn "Test suite not yet implemented"