/* $Id: RTFsCmdLs.cpp $ */ /** @file * IPRT - /bin/ls like utility for testing the VFS code. */ /* * Copyright (C) 2017-2023 Oracle and/or its affiliates. * * This file is part of VirtualBox base platform packages, as * available from https://www.virtualbox.org. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation, in version 3 of the * License. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . * * The contents of this file may alternatively be used under the terms * of the Common Development and Distribution License Version 1.0 * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included * in the VirtualBox distribution, in which case the provisions of the * CDDL are applicable instead of those of the GPL. * * You may elect to license modified versions of this file under the * terms and conditions of either the GPL or the CDDL or both. * * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 */ /********************************************************************************************************************************* * Header Files * *********************************************************************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include /********************************************************************************************************************************* * Structures and Typedefs * *********************************************************************************************************************************/ /** * Display entry. */ typedef struct RTCMDLSENTRY { /** The information about the entry. */ RTFSOBJINFO Info; /** Symbolic link target (allocated after the name). */ const char *pszTarget; /** Owner if applicable(allocated after the name). */ const char *pszOwner; /** Group if applicable (allocated after the name). */ const char *pszGroup; /** The length of szName. */ size_t cchName; /** The entry name. */ RT_FLEXIBLE_ARRAY_EXTENSION char szName[RT_FLEXIBLE_ARRAY]; } RTCMDLSENTRY; /** Pointer to a ls display entry. */ typedef RTCMDLSENTRY *PRTCMDLSENTRY; /** Pointer to a ls display entry pointer. */ typedef PRTCMDLSENTRY *PPRTCMDLSENTRY; /** * Collection of display entries. */ typedef struct RTCMDLSCOLLECTION { /** Current size of papEntries. */ size_t cEntries; /** Memory allocated for papEntries. */ size_t cEntriesAllocated; /** Current entries pending sorting and display. */ PPRTCMDLSENTRY papEntries; /** Total number of bytes allocated for the above entries. */ uint64_t cbTotalAllocated; /** Total number of file content bytes. */ uint64_t cbTotalFiles; /** The collection name (path). */ RT_FLEXIBLE_ARRAY_EXTENSION char szName[RT_FLEXIBLE_ARRAY]; } RTCMDLSCOLLECTION; /** Pointer to a display entry collection. */ typedef RTCMDLSCOLLECTION *PRTCMDLSCOLLECTION; /** Pointer to a display entry collection pointer. */ typedef PRTCMDLSCOLLECTION *PPRTCMDLSCOLLECTION; /** Sorting. */ typedef enum RTCMDLSSORT { RTCMDLSSORT_INVALID = 0, RTCMDLSSORT_NONE, RTCMDLSSORT_NAME, RTCMDLSSORT_EXTENSION, RTCMDLSSORT_SIZE, RTCMDLSSORT_TIME, RTCMDLSSORT_VERSION } RTCMDLSSORT; /** Time selection. */ typedef enum RTCMDLSTIME { RTCMDLSTIME_INVALID = 0, RTCMDLSTIME_BTIME, RTCMDLSTIME_CTIME, RTCMDLSTIME_MTIME, RTCMDLSTIME_ATIME } RTCMDLSTIME; /** Time display style. */ typedef enum RTCMDLSTIMESTYLE { RTCMDLSTIMESTYLE_INVALID = 0, RTCMDLSTIMESTYLE_FULL_ISO, RTCMDLSTIMESTYLE_LONG_ISO, RTCMDLSTIMESTYLE_ISO, RTCMDLSTIMESTYLE_LOCALE, RTCMDLSTIMESTYLE_CUSTOM } RTCMDLSTIMESTYLE; /** Coloring selection. */ typedef enum RTCMDLSCOLOR { RTCMDLSCOLOR_INVALID = 0, RTCMDLSCOLOR_NONE } RTCMDLSCOLOR; /** Formatting. */ typedef enum RTCMDLSFORMAT { RTCMDLSFORMAT_INVALID = 0, RTCMDLSFORMAT_COLS_VERTICAL, /**< -C/default */ RTCMDLSFORMAT_COLS_HORIZONTAL, /**< -x */ RTCMDLSFORMAT_COMMAS, /**< -m */ RTCMDLSFORMAT_SINGLE, /**< -1 */ RTCMDLSFORMAT_LONG, /**< -l */ RTCMDLSFORMAT_MACHINE_READABLE /**< --machine-readable */ } RTCMDLSFORMAT; /** * LS command options and state. */ typedef struct RTCMDLSOPTS { /** @name Traversal. * @{ */ bool fFollowSymlinksInDirs; /**< -L */ bool fFollowSymlinkToAnyArgs; bool fFollowSymlinkToDirArgs; bool fFollowDirectoryArgs; /**< Inverse -d/--directory. */ bool fRecursive; /**< -R */ /** @} */ /** @name Filtering. * @{ */ bool fShowHidden; /**< -a/--all or -A/--almost-all */ bool fShowDotAndDotDot; /**< -a vs -A */ bool fShowBackups; /**< Inverse -B/--ignore-backups (*~). */ /** @} */ /** @name Sorting * @{ */ RTCMDLSSORT enmSort; /**< --sort */ bool fReverseSort; /**< -r */ bool fGroupDirectoriesFirst; /**< fGroupDirectoriesFirst */ /** @} */ /** @name Formatting * @{ */ RTCMDLSFORMAT enmFormat; /**< --format */ bool fEscapeNonGraphicChars; /**< -b, --escape */ bool fEscapeControlChars; bool fHideControlChars; /**< -q/--hide-control-chars, --show-control-chars */ bool fHumanReadableSizes; /**< -h */ bool fSiUnits; /**< --si */ uint32_t cbBlock; /**< --block-size=N, -k */ bool fShowOwner; bool fShowGroup; bool fNumericalIds; /**< -n */ bool fShowINode; bool fShowAllocatedSize; /**< -s */ uint8_t cchTab; /**< -T */ uint32_t cchWidth; /**< -w */ RTCMDLSCOLOR enmColor; /**< --color */ RTCMDLSTIME enmTime; /**< --time */ RTCMDLSTIMESTYLE enmTimeStyle; /**< --time-style, --full-time */ const char *pszTimeCustom; /**< --time-style=+xxx */ /** @} */ /** @name State * @{ */ /** Current size of papCollections. */ size_t cCollections; /** Memory allocated for papCollections. */ size_t cCollectionsAllocated; /** Current entry collection pending display, the last may also be pending * sorting. */ PPRTCMDLSCOLLECTION papCollections; /** @} */ } RTCMDLSOPTS; /** Pointer to ls options and state. */ typedef RTCMDLSOPTS *PRTCMDLSOPTS; /** @callback_method_impl{FNRTSORTCMP, Dirs first + Unsorted} */ static DECLCALLBACK(int) rtCmdLsEntryCmpDirFirstUnsorted(void const *pvElement1, void const *pvElement2, void *pvUser) { RT_NOREF(pvUser); PRTCMDLSENTRY pEntry1 = (PRTCMDLSENTRY)pvElement1; PRTCMDLSENTRY pEntry2 = (PRTCMDLSENTRY)pvElement2; return !RTFS_IS_DIRECTORY(pEntry1->Info.Attr.fMode) - !RTFS_IS_DIRECTORY(pEntry2->Info.Attr.fMode); } /** @callback_method_impl{FNRTSORTCMP, Name} */ static DECLCALLBACK(int) rtCmdLsEntryCmpName(void const *pvElement1, void const *pvElement2, void *pvUser) { RT_NOREF(pvUser); PRTCMDLSENTRY pEntry1 = (PRTCMDLSENTRY)pvElement1; PRTCMDLSENTRY pEntry2 = (PRTCMDLSENTRY)pvElement2; return RTStrCmp(pEntry1->szName, pEntry2->szName); } /** @callback_method_impl{FNRTSORTCMP, Dirs first + Name} */ static DECLCALLBACK(int) rtCmdLsEntryCmpDirFirstName(void const *pvElement1, void const *pvElement2, void *pvUser) { RT_NOREF(pvUser); PRTCMDLSENTRY pEntry1 = (PRTCMDLSENTRY)pvElement1; PRTCMDLSENTRY pEntry2 = (PRTCMDLSENTRY)pvElement2; int iDiff = !RTFS_IS_DIRECTORY(pEntry1->Info.Attr.fMode) - !RTFS_IS_DIRECTORY(pEntry2->Info.Attr.fMode); if (!iDiff) iDiff = rtCmdLsEntryCmpName(pEntry1, pEntry2, pvUser); return iDiff; } /** @callback_method_impl{FNRTSORTCMP, extension} */ static DECLCALLBACK(int) rtCmdLsEntryCmpExtension(void const *pvElement1, void const *pvElement2, void *pvUser) { RT_NOREF(pvUser); PRTCMDLSENTRY pEntry1 = (PRTCMDLSENTRY)pvElement1; PRTCMDLSENTRY pEntry2 = (PRTCMDLSENTRY)pvElement2; int iDiff = RTStrCmp(RTPathSuffix(pEntry1->szName), RTPathSuffix(pEntry2->szName)); if (!iDiff) iDiff = RTStrCmp(pEntry1->szName, pEntry2->szName); return iDiff; } /** @callback_method_impl{FNRTSORTCMP, Dirs first + Ext + Name} */ static DECLCALLBACK(int) rtCmdLsEntryCmpDirFirstExtension(void const *pvElement1, void const *pvElement2, void *pvUser) { RT_NOREF(pvUser); PRTCMDLSENTRY pEntry1 = (PRTCMDLSENTRY)pvElement1; PRTCMDLSENTRY pEntry2 = (PRTCMDLSENTRY)pvElement2; int iDiff = !RTFS_IS_DIRECTORY(pEntry1->Info.Attr.fMode) - !RTFS_IS_DIRECTORY(pEntry2->Info.Attr.fMode); if (!iDiff) iDiff = rtCmdLsEntryCmpExtension(pEntry1, pEntry2, pvUser); return iDiff; } /** @callback_method_impl{FNRTSORTCMP, Allocated size + Name} */ static DECLCALLBACK(int) rtCmdLsEntryCmpAllocated(void const *pvElement1, void const *pvElement2, void *pvUser) { RT_NOREF(pvUser); PRTCMDLSENTRY pEntry1 = (PRTCMDLSENTRY)pvElement1; PRTCMDLSENTRY pEntry2 = (PRTCMDLSENTRY)pvElement2; if (pEntry1->Info.cbAllocated == pEntry2->Info.cbAllocated) return rtCmdLsEntryCmpName(pEntry1, pEntry2, pvUser); return pEntry1->Info.cbAllocated < pEntry2->Info.cbAllocated ? -1 : 1; } /** @callback_method_impl{FNRTSORTCMP, Dirs first + Allocated size + Name} */ static DECLCALLBACK(int) rtCmdLsEntryCmpDirFirstAllocated(void const *pvElement1, void const *pvElement2, void *pvUser) { RT_NOREF(pvUser); PRTCMDLSENTRY pEntry1 = (PRTCMDLSENTRY)pvElement1; PRTCMDLSENTRY pEntry2 = (PRTCMDLSENTRY)pvElement2; int iDiff = !RTFS_IS_DIRECTORY(pEntry1->Info.Attr.fMode) - !RTFS_IS_DIRECTORY(pEntry2->Info.Attr.fMode); if (!iDiff) iDiff = rtCmdLsEntryCmpAllocated(pEntry1, pEntry2, pvUser); return iDiff; } /** @callback_method_impl{FNRTSORTCMP, Content size + Name} */ static DECLCALLBACK(int) rtCmdLsEntryCmpSize(void const *pvElement1, void const *pvElement2, void *pvUser) { RT_NOREF(pvUser); PRTCMDLSENTRY pEntry1 = (PRTCMDLSENTRY)pvElement1; PRTCMDLSENTRY pEntry2 = (PRTCMDLSENTRY)pvElement2; if (pEntry1->Info.cbObject == pEntry2->Info.cbObject) return rtCmdLsEntryCmpName(pEntry1, pEntry2, pvUser); return pEntry1->Info.cbObject < pEntry2->Info.cbObject ? -1 : 1; } /** @callback_method_impl{FNRTSORTCMP, Dirs first + Content size + Name} */ static DECLCALLBACK(int) rtCmdLsEntryCmpDirFirstSize(void const *pvElement1, void const *pvElement2, void *pvUser) { PRTCMDLSENTRY pEntry1 = (PRTCMDLSENTRY)pvElement1; PRTCMDLSENTRY pEntry2 = (PRTCMDLSENTRY)pvElement2; int iDiff = !RTFS_IS_DIRECTORY(pEntry1->Info.Attr.fMode) - !RTFS_IS_DIRECTORY(pEntry2->Info.Attr.fMode); if (!iDiff) iDiff = rtCmdLsEntryCmpSize(pEntry1, pEntry2, pvUser); return iDiff; } /** @callback_method_impl{FNRTSORTCMP, Modification time + name} */ static DECLCALLBACK(int) rtCmdLsEntryCmpMTime(void const *pvElement1, void const *pvElement2, void *pvUser) { PRTCMDLSENTRY pEntry1 = (PRTCMDLSENTRY)pvElement1; PRTCMDLSENTRY pEntry2 = (PRTCMDLSENTRY)pvElement2; int iDiff = RTTimeSpecCompare(&pEntry1->Info.ModificationTime, &pEntry2->Info.ModificationTime); if (!iDiff) iDiff = rtCmdLsEntryCmpName(pEntry1, pEntry2, pvUser); return iDiff; } /** @callback_method_impl{FNRTSORTCMP, Dirs first + Modification time + Name} */ static DECLCALLBACK(int) rtCmdLsEntryCmpDirFirstMTime(void const *pvElement1, void const *pvElement2, void *pvUser) { RT_NOREF(pvUser); PRTCMDLSENTRY pEntry1 = (PRTCMDLSENTRY)pvElement1; PRTCMDLSENTRY pEntry2 = (PRTCMDLSENTRY)pvElement2; int iDiff = !RTFS_IS_DIRECTORY(pEntry1->Info.Attr.fMode) - !RTFS_IS_DIRECTORY(pEntry2->Info.Attr.fMode); if (!iDiff) iDiff = rtCmdLsEntryCmpMTime(pEntry1, pEntry2, pvUser); return iDiff; } /** @callback_method_impl{FNRTSORTCMP, Birth time + name} */ static DECLCALLBACK(int) rtCmdLsEntryCmpBTime(void const *pvElement1, void const *pvElement2, void *pvUser) { PRTCMDLSENTRY pEntry1 = (PRTCMDLSENTRY)pvElement1; PRTCMDLSENTRY pEntry2 = (PRTCMDLSENTRY)pvElement2; int iDiff = RTTimeSpecCompare(&pEntry1->Info.BirthTime, &pEntry2->Info.BirthTime); if (!iDiff) iDiff = rtCmdLsEntryCmpName(pEntry1, pEntry2, pvUser); return iDiff; } /** @callback_method_impl{FNRTSORTCMP, Dirs first + Birth time + Name} */ static DECLCALLBACK(int) rtCmdLsEntryCmpDirFirstBTime(void const *pvElement1, void const *pvElement2, void *pvUser) { RT_NOREF(pvUser); PRTCMDLSENTRY pEntry1 = (PRTCMDLSENTRY)pvElement1; PRTCMDLSENTRY pEntry2 = (PRTCMDLSENTRY)pvElement2; int iDiff = !RTFS_IS_DIRECTORY(pEntry1->Info.Attr.fMode) - !RTFS_IS_DIRECTORY(pEntry2->Info.Attr.fMode); if (!iDiff) iDiff = rtCmdLsEntryCmpBTime(pEntry1, pEntry2, pvUser); return iDiff; } /** @callback_method_impl{FNRTSORTCMP, Change time + name} */ static DECLCALLBACK(int) rtCmdLsEntryCmpCTime(void const *pvElement1, void const *pvElement2, void *pvUser) { PRTCMDLSENTRY pEntry1 = (PRTCMDLSENTRY)pvElement1; PRTCMDLSENTRY pEntry2 = (PRTCMDLSENTRY)pvElement2; int iDiff = RTTimeSpecCompare(&pEntry1->Info.ChangeTime, &pEntry2->Info.ChangeTime); if (!iDiff) iDiff = rtCmdLsEntryCmpName(pEntry1, pEntry2, pvUser); return iDiff; } /** @callback_method_impl{FNRTSORTCMP, Dirs first + Change time + Name} */ static DECLCALLBACK(int) rtCmdLsEntryCmpDirFirstCTime(void const *pvElement1, void const *pvElement2, void *pvUser) { RT_NOREF(pvUser); PRTCMDLSENTRY pEntry1 = (PRTCMDLSENTRY)pvElement1; PRTCMDLSENTRY pEntry2 = (PRTCMDLSENTRY)pvElement2; int iDiff = !RTFS_IS_DIRECTORY(pEntry1->Info.Attr.fMode) - !RTFS_IS_DIRECTORY(pEntry2->Info.Attr.fMode); if (!iDiff) iDiff = rtCmdLsEntryCmpCTime(pEntry1, pEntry2, pvUser); return iDiff; } /** @callback_method_impl{FNRTSORTCMP, Accessed time + name} */ static DECLCALLBACK(int) rtCmdLsEntryCmpATime(void const *pvElement1, void const *pvElement2, void *pvUser) { PRTCMDLSENTRY pEntry1 = (PRTCMDLSENTRY)pvElement1; PRTCMDLSENTRY pEntry2 = (PRTCMDLSENTRY)pvElement2; int iDiff = RTTimeSpecCompare(&pEntry1->Info.AccessTime, &pEntry2->Info.AccessTime); if (!iDiff) iDiff = rtCmdLsEntryCmpName(pEntry1, pEntry2, pvUser); return iDiff; } /** @callback_method_impl{FNRTSORTCMP, Dirs first + Accessed time + Name} */ static DECLCALLBACK(int) rtCmdLsEntryCmpDirFirstATime(void const *pvElement1, void const *pvElement2, void *pvUser) { RT_NOREF(pvUser); PRTCMDLSENTRY pEntry1 = (PRTCMDLSENTRY)pvElement1; PRTCMDLSENTRY pEntry2 = (PRTCMDLSENTRY)pvElement2; int iDiff = !RTFS_IS_DIRECTORY(pEntry1->Info.Attr.fMode) - !RTFS_IS_DIRECTORY(pEntry2->Info.Attr.fMode); if (!iDiff) iDiff = rtCmdLsEntryCmpATime(pEntry1, pEntry2, pvUser); return iDiff; } /** @callback_method_impl{FNRTSORTCMP, Name as version} */ static DECLCALLBACK(int) rtCmdLsEntryCmpVersion(void const *pvElement1, void const *pvElement2, void *pvUser) { RT_NOREF(pvUser); PRTCMDLSENTRY pEntry1 = (PRTCMDLSENTRY)pvElement1; PRTCMDLSENTRY pEntry2 = (PRTCMDLSENTRY)pvElement2; return RTStrVersionCompare(pEntry1->szName, pEntry2->szName); } /** @callback_method_impl{FNRTSORTCMP, Dirs first + Name as version} */ static DECLCALLBACK(int) rtCmdLsEntryCmpDirFirstVersion(void const *pvElement1, void const *pvElement2, void *pvUser) { RT_NOREF(pvUser); PRTCMDLSENTRY pEntry1 = (PRTCMDLSENTRY)pvElement1; PRTCMDLSENTRY pEntry2 = (PRTCMDLSENTRY)pvElement2; int iDiff = !RTFS_IS_DIRECTORY(pEntry1->Info.Attr.fMode) - !RTFS_IS_DIRECTORY(pEntry2->Info.Attr.fMode); if (!iDiff) iDiff = rtCmdLsEntryCmpVersion(pEntry1, pEntry2, pvUser); return iDiff; } /** * Sorts the entries in the collections according the sorting options. * * @param pOpts The options and state. */ static void rtCmdLsSortCollections(PRTCMDLSOPTS pOpts) { /* * Sort the entries in each collection. */ PFNRTSORTCMP pfnCmp; switch (pOpts->enmSort) { case RTCMDLSSORT_NONE: pfnCmp = pOpts->fGroupDirectoriesFirst ? rtCmdLsEntryCmpDirFirstUnsorted : NULL; break; default: AssertFailed(); RT_FALL_THRU(); case RTCMDLSSORT_NAME: pfnCmp = pOpts->fGroupDirectoriesFirst ? rtCmdLsEntryCmpDirFirstName : rtCmdLsEntryCmpName; break; case RTCMDLSSORT_EXTENSION: pfnCmp = pOpts->fGroupDirectoriesFirst ? rtCmdLsEntryCmpDirFirstExtension : rtCmdLsEntryCmpExtension; break; case RTCMDLSSORT_SIZE: if (pOpts->fShowAllocatedSize) pfnCmp = pOpts->fGroupDirectoriesFirst ? rtCmdLsEntryCmpDirFirstAllocated : rtCmdLsEntryCmpAllocated; else pfnCmp = pOpts->fGroupDirectoriesFirst ? rtCmdLsEntryCmpDirFirstSize : rtCmdLsEntryCmpSize; break; case RTCMDLSSORT_TIME: switch (pOpts->enmTime) { default: AssertFailed(); RT_FALL_THRU(); case RTCMDLSTIME_MTIME: pfnCmp = pOpts->fGroupDirectoriesFirst ? rtCmdLsEntryCmpDirFirstMTime : rtCmdLsEntryCmpMTime; break; case RTCMDLSTIME_BTIME: pfnCmp = pOpts->fGroupDirectoriesFirst ? rtCmdLsEntryCmpDirFirstBTime : rtCmdLsEntryCmpBTime; break; case RTCMDLSTIME_CTIME: pfnCmp = pOpts->fGroupDirectoriesFirst ? rtCmdLsEntryCmpDirFirstCTime : rtCmdLsEntryCmpCTime; break; case RTCMDLSTIME_ATIME: pfnCmp = pOpts->fGroupDirectoriesFirst ? rtCmdLsEntryCmpDirFirstATime : rtCmdLsEntryCmpATime; break; } break; case RTCMDLSSORT_VERSION: pfnCmp = pOpts->fGroupDirectoriesFirst ? rtCmdLsEntryCmpDirFirstVersion : rtCmdLsEntryCmpVersion; break; } if (pfnCmp) { /* * Walk thru the collections and sort their entries. */ size_t i = pOpts->cCollections; while (i-- > 0) { PRTCMDLSCOLLECTION pCollection = pOpts->papCollections[i]; RTSortApvShell((void **)pCollection->papEntries, pCollection->cEntries, pfnCmp, NULL); if (pOpts->fReverseSort) { PPRTCMDLSENTRY papEntries = pCollection->papEntries; size_t iHead = 0; size_t iTail = pCollection->cEntries; while (iHead < iTail) { PRTCMDLSENTRY pTmp = papEntries[iHead]; papEntries[iHead] = papEntries[iTail]; papEntries[iTail] = pTmp; iHead++; iTail--; } } } } /** @todo sort the collections too, except for the first one. */ } /** * Format human readable size. */ static const char *rtCmdLsFormatSizeHumanReadable(PRTCMDLSOPTS pOpts, uint64_t cb, char *pszDst, size_t cbDst) { if (pOpts->fHumanReadableSizes) { if (!pOpts->fSiUnits) { size_t cch = RTStrPrintf(pszDst, cbDst, "%Rhub", cb); if (pszDst[cch - 1] == 'i') pszDst[cch - 1] = '\0'; /* drop the trailing 'i' */ } else RTStrPrintf(pszDst, cbDst, "%Rhui", cb); } else if (pOpts->cbBlock) RTStrFormatU64(pszDst, cbDst, (cb + pOpts->cbBlock - 1) / pOpts->cbBlock, 10, 0, 0, 0); else RTStrFormatU64(pszDst, cbDst, cb, 10, 0, 0, 0); return pszDst; } /** * Format block count. */ static const char *rtCmdLsFormatBlocks(PRTCMDLSOPTS pOpts, uint64_t cb, char *pszDst, size_t cbDst) { if (pOpts->fHumanReadableSizes) return rtCmdLsFormatSizeHumanReadable(pOpts, cb, pszDst, cbDst); uint32_t cbBlock = pOpts->cbBlock; if (cbBlock == 0) cbBlock = _1K; RTStrFormatU64(pszDst, cbDst, (cb + cbBlock / 2 - 1) / cbBlock, 10, 0, 0, 0); return pszDst; } /** * Format file size. */ static const char *rtCmdLsFormatSize(PRTCMDLSOPTS pOpts, uint64_t cb, char *pszDst, size_t cbDst) { if (pOpts->fHumanReadableSizes) return rtCmdLsFormatSizeHumanReadable(pOpts, cb, pszDst, cbDst); if (pOpts->cbBlock > 0) return rtCmdLsFormatBlocks(pOpts, cb, pszDst, cbDst); RTStrFormatU64(pszDst, cbDst, cb, 10, 0, 0, 0); return pszDst; } /** * Format name, i.e. escape, hide, quote stuff. */ static const char *rtCmdLsFormatName(PRTCMDLSOPTS pOpts, const char *pszName, char *pszDst, size_t cbDst) { if ( !pOpts->fEscapeNonGraphicChars && !pOpts->fEscapeControlChars && !pOpts->fHideControlChars) return pszName; /** @todo implement name formatting. */ RT_NOREF(pszDst, cbDst); return pszName; } /** * Figures out the length for a 32-bit number when formatted as decimal. * @returns Number of digits. * @param uValue The number. */ DECLINLINE(size_t) rtCmdLsDecimalFormatLengthU32(uint32_t uValue) { if (uValue < 10) return 1; if (uValue < 100) return 2; if (uValue < 1000) return 3; if (uValue < 10000) return 4; if (uValue < 100000) return 5; if (uValue < 1000000) return 6; if (uValue < 10000000) return 7; if (uValue < 100000000) return 8; if (uValue < 1000000000) return 9; return 10; } /** * Formats the given group ID according to the specified options. * * @returns pszDst * @param pOpts The options and state. * @param gid The GID to format. * @param pszOwner The owner returned by the FS. * @param pszDst The output buffer. * @param cbDst The output buffer size. */ static const char *rtCmdLsDecimalFormatGroup(PRTCMDLSOPTS pOpts, RTGID gid, const char *pszGroup, char *pszDst, size_t cbDst) { if (!pOpts->fNumericalIds) { if (pszGroup) { RTStrCopy(pszDst, cbDst, pszGroup); return pszDst; } if (gid == NIL_RTGID) return ""; } RTStrFormatU64(pszDst, cbDst, gid, 10, 0, 0, 0); return pszDst; } /** * Formats the given user ID according to the specified options. * * @returns pszDst * @param pOpts The options and state. * @param uid The UID to format. * @param pszOwner The owner returned by the FS. * @param pszDst The output buffer. * @param cbDst The output buffer size. */ static const char *rtCmdLsDecimalFormatOwner(PRTCMDLSOPTS pOpts, RTUID uid, const char *pszOwner, char *pszDst, size_t cbDst) { if (!pOpts->fNumericalIds) { if (pszOwner) { RTStrCopy(pszDst, cbDst, pszOwner); return pszDst; } if (uid == NIL_RTUID) return ""; } RTStrFormatU64(pszDst, cbDst, uid, 10, 0, 0, 0); return pszDst; } /** * Formats the given timestamp according to the desired --time-style. * * @returns pszDst * @param pOpts The options and state. * @param pTimestamp The timestamp. * @param pszDst The output buffer. * @param cbDst The output buffer size. */ static const char *rtCmdLsFormatTimestamp(PRTCMDLSOPTS pOpts, PCRTTIMESPEC pTimestamp, char *pszDst, size_t cbDst) { /** @todo timestamp formatting according to the given style. */ RT_NOREF(pOpts); return RTTimeSpecToString(pTimestamp, pszDst, cbDst); } /** * RTCMDLSFORMAT_MACHINE_READABLE: --machine-readable */ static RTEXITCODE rtCmdLsDisplayCollectionInMachineReadableFormat(PRTCMDLSOPTS pOpts, PRTCMDLSCOLLECTION pCollection, char *pszTmp, size_t cbTmp) { RT_NOREF(pOpts, pCollection, pszTmp, cbTmp); RTMsgError("Machine readable format not implemented\n"); return RTEXITCODE_FAILURE; } /** * RTCMDLSFORMAT_COMMAS: -m */ static RTEXITCODE rtCmdLsDisplayCollectionInCvsFormat(PRTCMDLSOPTS pOpts, PRTCMDLSCOLLECTION pCollection, char *pszTmp, size_t cbTmp) { RT_NOREF(pOpts, pCollection, pszTmp, cbTmp); RTMsgError("Table output formats not implemented\n"); return RTEXITCODE_FAILURE; } /** * RTCMDLSFORMAT_LONG: -l */ static RTEXITCODE rtCmdLsDisplayCollectionInLongFormat(PRTCMDLSOPTS pOpts, PRTCMDLSCOLLECTION pCollection, char *pszTmp, size_t cbTmp, size_t cchAllocatedCol) { /* * Figure the width of the size, the link count, the uid, the gid, and the inode columns. */ size_t cchSizeCol = 1; size_t cchLinkCol = 1; size_t cchUidCol = pOpts->fShowOwner ? 1 : 0; size_t cchGidCol = pOpts->fShowGroup ? 1 : 0; size_t cchINodeCol = pOpts->fShowINode ? 1 : 0; size_t i = pCollection->cEntries; while (i-- > 0) { PRTCMDLSENTRY pEntry = pCollection->papEntries[i]; rtCmdLsFormatSize(pOpts, pEntry->Info.cbObject, pszTmp, cbTmp); size_t cchTmp = strlen(pszTmp); if (cchTmp > cchSizeCol) cchSizeCol = cchTmp; cchTmp = rtCmdLsDecimalFormatLengthU32(pEntry->Info.Attr.u.Unix.cHardlinks) + 1; if (cchTmp > cchLinkCol) cchLinkCol = cchTmp; if (pOpts->fShowOwner) { rtCmdLsDecimalFormatOwner(pOpts, pEntry->Info.Attr.u.Unix.uid, pEntry->pszOwner, pszTmp, cbTmp); cchTmp = strlen(pszTmp); if (cchTmp > cchUidCol) cchUidCol = cchTmp; } if (pOpts->fShowGroup) { rtCmdLsDecimalFormatGroup(pOpts, pEntry->Info.Attr.u.Unix.gid, pEntry->pszGroup, pszTmp, cbTmp); cchTmp = strlen(pszTmp); if (cchTmp > cchGidCol) cchGidCol = cchTmp; } if (pOpts->fShowINode) { cchTmp = RTStrFormatU64(pszTmp, cchTmp, pEntry->Info.Attr.u.Unix.INodeId, 10, 0, 0, 0); if (cchTmp > cchINodeCol) cchINodeCol = cchTmp; } } /* * Determin time member offset. */ size_t offTime; switch (pOpts->enmTime) { default: AssertFailed(); RT_FALL_THRU(); case RTCMDLSTIME_MTIME: offTime = RT_UOFFSETOF(RTCMDLSENTRY, Info.ModificationTime); break; case RTCMDLSTIME_BTIME: offTime = RT_UOFFSETOF(RTCMDLSENTRY, Info.BirthTime); break; case RTCMDLSTIME_CTIME: offTime = RT_UOFFSETOF(RTCMDLSENTRY, Info.ChangeTime); break; case RTCMDLSTIME_ATIME: offTime = RT_UOFFSETOF(RTCMDLSENTRY, Info.AccessTime); break; } /* * Display the entries. */ for (i = 0; i < pCollection->cEntries; i++) { PRTCMDLSENTRY pEntry = pCollection->papEntries[i]; if (cchINodeCol) RTPrintf("%*RU64 ", cchINodeCol, pEntry->Info.Attr.u.Unix.INodeId); if (cchAllocatedCol) RTPrintf("%*s ", cchAllocatedCol, rtCmdLsFormatBlocks(pOpts, pEntry->Info.cbAllocated, pszTmp, cbTmp)); RTFMODE fMode = pEntry->Info.Attr.fMode; switch (fMode & RTFS_TYPE_MASK) { case RTFS_TYPE_FIFO: RTPrintf("f"); break; case RTFS_TYPE_DEV_CHAR: RTPrintf("c"); break; case RTFS_TYPE_DIRECTORY: RTPrintf("d"); break; case RTFS_TYPE_DEV_BLOCK: RTPrintf("b"); break; case RTFS_TYPE_FILE: RTPrintf("-"); break; case RTFS_TYPE_SYMLINK: RTPrintf("l"); break; case RTFS_TYPE_SOCKET: RTPrintf("s"); break; case RTFS_TYPE_WHITEOUT: RTPrintf("w"); break; default: RTPrintf("?"); AssertFailed(); break; } /** @todo sticy bits++ */ RTPrintf("%c%c%c", fMode & RTFS_UNIX_IRUSR ? 'r' : '-', fMode & RTFS_UNIX_IWUSR ? 'w' : '-', fMode & RTFS_UNIX_IXUSR ? 'x' : '-'); RTPrintf("%c%c%c", fMode & RTFS_UNIX_IRGRP ? 'r' : '-', fMode & RTFS_UNIX_IWGRP ? 'w' : '-', fMode & RTFS_UNIX_IXGRP ? 'x' : '-'); RTPrintf("%c%c%c", fMode & RTFS_UNIX_IROTH ? 'r' : '-', fMode & RTFS_UNIX_IWOTH ? 'w' : '-', fMode & RTFS_UNIX_IXOTH ? 'x' : '-'); if (1) { RTPrintf(" %c%c%c%c%c%c%c%c%c%c%c%c%c%c", fMode & RTFS_DOS_READONLY ? 'R' : '-', fMode & RTFS_DOS_HIDDEN ? 'H' : '-', fMode & RTFS_DOS_SYSTEM ? 'S' : '-', fMode & RTFS_DOS_DIRECTORY ? 'D' : '-', fMode & RTFS_DOS_ARCHIVED ? 'A' : '-', fMode & RTFS_DOS_NT_DEVICE ? 'd' : '-', fMode & RTFS_DOS_NT_NORMAL ? 'N' : '-', fMode & RTFS_DOS_NT_TEMPORARY ? 'T' : '-', fMode & RTFS_DOS_NT_SPARSE_FILE ? 'P' : '-', fMode & RTFS_DOS_NT_REPARSE_POINT ? 'J' : '-', fMode & RTFS_DOS_NT_COMPRESSED ? 'C' : '-', fMode & RTFS_DOS_NT_OFFLINE ? 'O' : '-', fMode & RTFS_DOS_NT_NOT_CONTENT_INDEXED ? 'I' : '-', fMode & RTFS_DOS_NT_ENCRYPTED ? 'E' : '-'); } RTPrintf(" %*u", cchLinkCol, pEntry->Info.Attr.u.Unix.cHardlinks); if (cchUidCol) RTPrintf(" %*s", cchUidCol, rtCmdLsDecimalFormatOwner(pOpts, pEntry->Info.Attr.u.Unix.uid, pEntry->pszOwner, pszTmp, cbTmp)); if (cchGidCol) RTPrintf(" %*s", cchGidCol, rtCmdLsDecimalFormatGroup(pOpts, pEntry->Info.Attr.u.Unix.gid, pEntry->pszGroup, pszTmp, cbTmp)); RTPrintf(" %*s", cchSizeCol, rtCmdLsFormatSize(pOpts, pEntry->Info.cbObject, pszTmp, cbTmp)); PCRTTIMESPEC pTime = (PCRTTIMESPEC)((uintptr_t)pEntry + offTime); RTPrintf(" %s", rtCmdLsFormatTimestamp(pOpts, pTime, pszTmp, cbTmp)); RTPrintf(" %s\n", rtCmdLsFormatName(pOpts, pEntry->szName, pszTmp, cbTmp)); } return RTEXITCODE_SUCCESS; } /** * RTCMDLSFORMAT_SINGLE: -1 */ static RTEXITCODE rtCmdLsDisplayCollectionInSingleFormat(PRTCMDLSOPTS pOpts, PRTCMDLSCOLLECTION pCollection, char *pszTmp, size_t cbTmp, size_t cchAllocatedCol) { if (cchAllocatedCol > 0) for (size_t i = 0; i < pCollection->cEntries; i++) { PRTCMDLSENTRY pEntry = pCollection->papEntries[i]; RTPrintf("%*s %s\n", cchAllocatedCol, rtCmdLsFormatBlocks(pOpts, pEntry->Info.cbAllocated, pszTmp, cbTmp / 4), rtCmdLsFormatName(pOpts, pEntry->szName, &pszTmp[cbTmp / 4], cbTmp / 4 * 3)); } else for (size_t i = 0; i < pCollection->cEntries; i++) { PRTCMDLSENTRY pEntry = pCollection->papEntries[i]; RTPrintf("%s\n", rtCmdLsFormatName(pOpts, pEntry->szName, pszTmp, cbTmp)); } return RTEXITCODE_SUCCESS; } /** * RTCMDLSFORMAT_COLS_VERTICAL: default, -C; RTCMDLSFORMAT_COLS_HORIZONTAL: -x */ static RTEXITCODE rtCmdLsDisplayCollectionInTableFormat(PRTCMDLSOPTS pOpts, PRTCMDLSCOLLECTION pCollection, char *pszTmp, size_t cbTmp, size_t cchAllocatedCol) { RT_NOREF(pOpts, pCollection, pszTmp, cbTmp, cchAllocatedCol); RTMsgError("Table output formats not implemented\n"); return RTEXITCODE_FAILURE; } /** * Does the actual displaying of the entry collections. * * @returns Program exit code. * @param pOpts The options and state. */ static RTEXITCODE rtCmdLsDisplayCollections(PRTCMDLSOPTS pOpts) { rtCmdLsSortCollections(pOpts); bool const fNeedCollectionName = pOpts->cCollections > 2 || ( pOpts->cCollections == 2 && pOpts->papCollections[0]->cEntries > 0); RTEXITCODE rcExit = RTEXITCODE_SUCCESS; for (size_t iCollection = 0; iCollection < pOpts->cCollections; iCollection++) { PRTCMDLSCOLLECTION pCollection = pOpts->papCollections[iCollection]; char szTmp[RTPATH_MAX*2]; /* The header. */ if (iCollection != 0) { if ( iCollection > 1 || pOpts->papCollections[0]->cEntries > 0) RTPrintf("\n"); if (fNeedCollectionName) RTPrintf("%s:\n", rtCmdLsFormatName(pOpts, pCollection->szName, szTmp, sizeof(szTmp))); RTPrintf("total %s\n", rtCmdLsFormatBlocks(pOpts, pCollection->cbTotalAllocated, szTmp, sizeof(szTmp))); } /* Format the entries. */ RTEXITCODE rcExit2; if (pOpts->enmFormat == RTCMDLSFORMAT_MACHINE_READABLE) rcExit2 = rtCmdLsDisplayCollectionInMachineReadableFormat(pOpts, pCollection, szTmp, sizeof(szTmp)); else if (pOpts->enmFormat == RTCMDLSFORMAT_COMMAS) rcExit2 = rtCmdLsDisplayCollectionInCvsFormat(pOpts, pCollection, szTmp, sizeof(szTmp)); else { /* If the allocated size is requested, calculate the column width. */ size_t cchAllocatedCol = 0; if (pOpts->fShowAllocatedSize) { size_t i = pCollection->cEntries; while (i-- > 0) { rtCmdLsFormatBlocks(pOpts, pCollection->papEntries[i]->Info.cbAllocated, szTmp, sizeof(szTmp)); size_t cchTmp = strlen(szTmp); if (cchTmp > cchAllocatedCol) cchAllocatedCol = cchTmp; } } /* Do the individual formatting. */ if (pOpts->enmFormat == RTCMDLSFORMAT_LONG) rcExit2 = rtCmdLsDisplayCollectionInLongFormat(pOpts, pCollection, szTmp, sizeof(szTmp), cchAllocatedCol); else if (pOpts->enmFormat == RTCMDLSFORMAT_SINGLE) rcExit2 = rtCmdLsDisplayCollectionInSingleFormat(pOpts, pCollection, szTmp, sizeof(szTmp), cchAllocatedCol); else rcExit2 = rtCmdLsDisplayCollectionInTableFormat(pOpts, pCollection, szTmp, sizeof(szTmp), cchAllocatedCol); } if (rcExit2 != RTEXITCODE_SUCCESS) rcExit = rcExit2; } return rcExit; } /** * Frees all collections and their entries. * @param pOpts The options and state. */ static void rtCmdLsFreeCollections(PRTCMDLSOPTS pOpts) { size_t i = pOpts->cCollections; while (i-- > 0) { PRTCMDLSCOLLECTION pCollection = pOpts->papCollections[i]; PPRTCMDLSENTRY papEntries = pCollection->papEntries; size_t j = pCollection->cEntries; while (j-- > 0) { RTMemFree(papEntries[j]); papEntries[j] = NULL; } RTMemFree(papEntries); pCollection->papEntries = NULL; pCollection->cEntries = 0; pCollection->cEntriesAllocated = 0; RTMemFree(pCollection); pOpts->papCollections[i] = NULL; } RTMemFree(pOpts->papCollections); pOpts->papCollections = NULL; pOpts->cCollections = 0; pOpts->cCollectionsAllocated = 0; } /** * Allocates a new collection. * * @returns Pointer to the collection. * @param pOpts The options and state. * @param pszName The collection name. Empty for special first * collection. */ static PRTCMDLSCOLLECTION rtCmdLsNewCollection(PRTCMDLSOPTS pOpts, const char *pszName) { /* Grow the pointer table? */ if (pOpts->cCollections >= pOpts->cCollectionsAllocated) { size_t cNew = pOpts->cCollectionsAllocated ? pOpts->cCollectionsAllocated * 2 : 16; void *pvNew = RTMemRealloc(pOpts->papCollections, cNew * sizeof(pOpts->papCollections[0])); if (!pvNew) { RTMsgError("Out of memory! (resize collections)"); return NULL; } pOpts->cCollectionsAllocated = cNew; pOpts->papCollections = (PPRTCMDLSCOLLECTION)pvNew; /* If this is the first time and pszName isn't empty, add the zero'th entry for the command line stuff (hardcoded first collection). */ if ( pOpts->cCollections == 0 && *pszName) { PRTCMDLSCOLLECTION pCollection = (PRTCMDLSCOLLECTION)RTMemAllocZ(RT_UOFFSETOF(RTCMDLSCOLLECTION, szName[1])); if (!pCollection) { RTMsgError("Out of memory! (collection)"); return NULL; } pOpts->papCollections[0] = pCollection; pOpts->cCollections = 1; } } /* Add new collection. */ size_t cbName = strlen(pszName) + 1; PRTCMDLSCOLLECTION pCollection = (PRTCMDLSCOLLECTION)RTMemAllocZ(RT_UOFFSETOF_DYN(RTCMDLSCOLLECTION, szName[cbName])); if (pCollection) { memcpy(pCollection->szName, pszName, cbName); pOpts->papCollections[pOpts->cCollections++] = pCollection; } else RTMsgError("Out of memory! (collection)"); return pCollection; } /** * Adds one entry to a collection. * @returns Program exit code * @param pCollection The collection. * @param pszEntry The entry name. * @param pInfo The entry info. * @param pszOwner The owner name if available, otherwise NULL. * @param pszGroup The group anme if available, otherwise NULL. * @param pszTarget The symbolic link target if applicable and * available, otherwise NULL. */ static RTEXITCODE rtCmdLsAddOne(PRTCMDLSCOLLECTION pCollection, const char *pszEntry, PRTFSOBJINFO pInfo, const char *pszOwner, const char *pszGroup, const char *pszTarget) { /* Make sure there is space in the collection for the new entry. */ if (pCollection->cEntries >= pCollection->cEntriesAllocated) { size_t cNew = pCollection->cEntriesAllocated ? pCollection->cEntriesAllocated * 2 : 16; void *pvNew = RTMemRealloc(pCollection->papEntries, cNew * sizeof(pCollection->papEntries[0])); if (!pvNew) return RTMsgErrorExitFailure("Out of memory! (resize entries)"); pCollection->papEntries = (PPRTCMDLSENTRY)pvNew; pCollection->cEntriesAllocated = cNew; } /* Create and insert a new entry. */ size_t const cchEntry = strlen(pszEntry); size_t const cbOwner = pszOwner ? strlen(pszOwner) + 1 : 0; size_t const cbGroup = pszGroup ? strlen(pszGroup) + 1 : 0; size_t const cbTarget = pszTarget ? strlen(pszTarget) + 1 : 0; size_t const cbEntry = RT_UOFFSETOF_DYN(RTCMDLSENTRY, szName[cchEntry + 1 + cbOwner + cbGroup + cbTarget]); PRTCMDLSENTRY pEntry = (PRTCMDLSENTRY)RTMemAlloc(cbEntry); if (pEntry) { pEntry->Info = *pInfo; pEntry->pszTarget = NULL; /** @todo symbolic links. */ pEntry->pszOwner = NULL; pEntry->pszGroup = NULL; pEntry->cchName = cchEntry; memcpy(pEntry->szName, pszEntry, cchEntry); pEntry->szName[cchEntry] = '\0'; char *psz = &pEntry->szName[cchEntry + 1]; if (pszTarget) { pEntry->pszTarget = psz; memcpy(psz, pszTarget, cbTarget); psz += cbTarget; } if (pszOwner) { pEntry->pszOwner = psz; memcpy(psz, pszOwner, cbOwner); psz += cbOwner; } if (pszGroup) { pEntry->pszGroup = psz; memcpy(psz, pszGroup, cbGroup); } pCollection->papEntries[pCollection->cEntries++] = pEntry; pCollection->cbTotalAllocated += pEntry->Info.cbAllocated; pCollection->cbTotalFiles += pEntry->Info.cbObject; return RTEXITCODE_SUCCESS; } return RTMsgErrorExitFailure("Out of memory! (entry)"); } /** * Checks if the entry is to be filtered out. * * @returns true if filtered out, false if included. * @param pOpts The options and state. * @param pszEntry The entry name. * @param pInfo The entry info. */ static bool rtCmdLsIsFilteredOut(PRTCMDLSOPTS pOpts, const char *pszEntry, PCRTFSOBJINFO pInfo) { /* * Should we filter out this entry? */ if ( !pOpts->fShowHidden && (pInfo->Attr.fMode & RTFS_DOS_HIDDEN)) return true; size_t const cchEntry = strlen(pszEntry); if ( !pOpts->fShowDotAndDotDot && cchEntry <= 2 && pszEntry[0] == '.' && ( cchEntry == 1 || pszEntry[1] == '.' )) return true; if ( !pOpts->fShowBackups && pszEntry[cchEntry - 1] == '~') return true; return false; } /** * Processes a directory, recursing into subdirectories if desired. * * @returns Program exit code. * @param pOpts The options. * @param hVfsDir The directory. * @param pszPath Path buffer, RTPATH_MAX in size. * @param cchPath The length of the current path. * @param pInfo The parent information. */ static RTEXITCODE rtCmdLsProcessDirectory(PRTCMDLSOPTS pOpts, RTVFSDIR hVfsDir, char *pszPath, size_t cchPath, PCRTFSOBJINFO pInfo) { /* * Create a new collection for this directory. */ RT_NOREF(pInfo); PRTCMDLSCOLLECTION pCollection = rtCmdLsNewCollection(pOpts, pszPath); if (!pCollection) return RTEXITCODE_FAILURE; /* * Process the directory entries. */ RTEXITCODE rcExit = RTEXITCODE_SUCCESS; size_t cbDirEntryAlloced = sizeof(RTDIRENTRYEX); PRTDIRENTRYEX pDirEntry = (PRTDIRENTRYEX)RTMemTmpAlloc(cbDirEntryAlloced); if (!pDirEntry) return RTMsgErrorExitFailure("Out of memory! (direntry buffer)"); for (;;) { /* * Read the next entry. */ size_t cbDirEntry = cbDirEntryAlloced; int rc = RTVfsDirReadEx(hVfsDir, pDirEntry, &cbDirEntry, RTFSOBJATTRADD_UNIX); if (RT_FAILURE(rc)) { if (rc == VERR_BUFFER_OVERFLOW) { RTMemTmpFree(pDirEntry); cbDirEntryAlloced = RT_ALIGN_Z(RT_MIN(cbDirEntry, cbDirEntryAlloced) + 64, 64); pDirEntry = (PRTDIRENTRYEX)RTMemTmpAlloc(cbDirEntryAlloced); if (pDirEntry) continue; rcExit = RTMsgErrorExitFailure("Out of memory (direntry buffer)"); } else if (rc != VERR_NO_MORE_FILES) rcExit = RTMsgErrorExitFailure("RTVfsDirReadEx failed: %Rrc\n", rc); break; } /* * Process the entry. */ if (rtCmdLsIsFilteredOut(pOpts, pDirEntry->szName, &pDirEntry->Info)) continue; const char *pszOwner = NULL; RTFSOBJINFO OwnerInfo; if (pDirEntry->Info.Attr.u.Unix.uid != NIL_RTUID && pOpts->fShowOwner) { rc = RTVfsDirQueryPathInfo(hVfsDir, pDirEntry->szName, &OwnerInfo, RTFSOBJATTRADD_UNIX_OWNER, RTPATH_F_ON_LINK); if (RT_SUCCESS(rc) && OwnerInfo.Attr.u.UnixOwner.szName[0]) pszOwner = &OwnerInfo.Attr.u.UnixOwner.szName[0]; } const char *pszGroup = NULL; RTFSOBJINFO GroupInfo; if (pDirEntry->Info.Attr.u.Unix.gid != NIL_RTGID && pOpts->fShowGroup) { rc = RTVfsDirQueryPathInfo(hVfsDir, pDirEntry->szName, &GroupInfo, RTFSOBJATTRADD_UNIX_GROUP, RTPATH_F_ON_LINK); if (RT_SUCCESS(rc) && GroupInfo.Attr.u.UnixGroup.szName[0]) pszGroup = &GroupInfo.Attr.u.UnixGroup.szName[0]; } RTEXITCODE rcExit2 = rtCmdLsAddOne(pCollection, pDirEntry->szName, &pDirEntry->Info, pszOwner, pszGroup, NULL); if (rcExit2 != RTEXITCODE_SUCCESS) rcExit = rcExit2; } RTMemTmpFree(pDirEntry); /* * Recurse into subdirectories if requested. */ if (pOpts->fRecursive) { for (uint32_t i = 0; i < pCollection->cEntries; i++) { PRTCMDLSENTRY pEntry = pCollection->papEntries[i]; if (RTFS_IS_SYMLINK(pEntry->Info.Attr.fMode)) { if (!pOpts->fFollowSymlinksInDirs) continue; /** @todo implement following symbolic links in the tree. */ continue; } else if ( !RTFS_IS_DIRECTORY(pEntry->Info.Attr.fMode) || ( pEntry->szName[0] == '.' && ( pEntry->szName[1] == '\0' || ( pEntry->szName[1] == '.' && pEntry->szName[2] == '\0'))) ) continue; /* Open subdirectory and process it. */ RTVFSDIR hSubDir; int rc = RTVfsDirOpenDir(hVfsDir, pEntry->szName, 0 /*fFlags*/, &hSubDir); if (RT_SUCCESS(rc)) { if (cchPath + 1 + pEntry->cchName + 1 < RTPATH_MAX) { pszPath[cchPath] = RTPATH_SLASH; memcpy(&pszPath[cchPath + 1], pEntry->szName, pEntry->cchName + 1); RTEXITCODE rcExit2 = rtCmdLsProcessDirectory(pOpts, hSubDir, pszPath, cchPath + 1 + pEntry->cchName, &pEntry->Info); if (rcExit2 != RTEXITCODE_SUCCESS) rcExit = rcExit2; pszPath[cchPath] = '\0'; } else rcExit = RTMsgErrorExitFailure("Too deep recursion: %s%c%s", pszPath, RTPATH_SLASH, pEntry->szName); RTVfsDirRelease(hSubDir); } else rcExit = RTMsgErrorExitFailure("RTVfsDirOpenDir failed on %s in %s: %Rrc\n", pEntry->szName, pszPath, rc); } } return rcExit; } /** * Processes one argument. * * @returns Program exit code. * @param pOpts The options. * @param pszArg The argument. */ static RTEXITCODE rtCmdLsProcessArgument(PRTCMDLSOPTS pOpts, const char *pszArg) { /* * Query info about the object 'pszArg' indicates. */ RTERRINFOSTATIC ErrInfo; uint32_t offError; RTFSOBJINFO Info; uint32_t fPath = pOpts->fFollowSymlinkToAnyArgs ? RTPATH_F_FOLLOW_LINK : RTPATH_F_ON_LINK; int rc = RTVfsChainQueryInfo(pszArg, &Info, RTFSOBJATTRADD_UNIX, fPath, &offError, RTErrInfoInitStatic(&ErrInfo)); if (RT_FAILURE(rc)) return RTVfsChainMsgErrorExitFailure("RTVfsChainQueryInfo", pszArg, rc, offError, &ErrInfo.Core); /* Symbolic links requires special handling of course. */ if (RTFS_IS_SYMLINK(Info.Attr.fMode)) { if (pOpts->fFollowSymlinkToDirArgs) { RTFSOBJINFO Info2; rc = RTVfsChainQueryInfo(pszArg, &Info2, RTFSOBJATTRADD_UNIX, RTPATH_F_FOLLOW_LINK, &offError, RTErrInfoInitStatic(&ErrInfo)); if (RT_SUCCESS(rc) && !RTFS_IS_DIRECTORY(Info.Attr.fMode)) Info = Info2; } } /* * If it's not a directory or we've been told to process directories * without going into them, just add it to the default collection. */ if ( !pOpts->fFollowDirectoryArgs || !RTFS_IS_DIRECTORY(Info.Attr.fMode)) { if ( pOpts->cCollections > 0 || rtCmdLsNewCollection(pOpts, "") != NULL) { const char *pszOwner = NULL; RTFSOBJINFO OwnerInfo; if (Info.Attr.u.Unix.uid != NIL_RTUID && pOpts->fShowOwner) { rc = RTVfsChainQueryInfo(pszArg, &OwnerInfo, RTFSOBJATTRADD_UNIX_OWNER, fPath, NULL, NULL); if (RT_SUCCESS(rc) && OwnerInfo.Attr.u.UnixOwner.szName[0]) pszOwner = &OwnerInfo.Attr.u.UnixOwner.szName[0]; } const char *pszGroup = NULL; RTFSOBJINFO GroupInfo; if (Info.Attr.u.Unix.gid != NIL_RTGID && pOpts->fShowGroup) { rc = RTVfsChainQueryInfo(pszArg, &GroupInfo, RTFSOBJATTRADD_UNIX_GROUP, fPath, NULL, NULL); if (RT_SUCCESS(rc) && GroupInfo.Attr.u.UnixGroup.szName[0]) pszGroup = &GroupInfo.Attr.u.UnixGroup.szName[0]; } return rtCmdLsAddOne(pOpts->papCollections[0], pszArg, &Info, pszOwner, pszGroup, NULL); } return RTEXITCODE_FAILURE; } /* * Open the directory. */ RTVFSDIR hVfsDir; rc = RTVfsChainOpenDir(pszArg, 0 /*fFlags*/, &hVfsDir, &offError, RTErrInfoInitStatic(&ErrInfo)); if (RT_FAILURE(rc)) return RTVfsChainMsgErrorExitFailure("RTVfsChainOpenDir", pszArg, rc, offError, &ErrInfo.Core); RTEXITCODE rcExit; char szPath[RTPATH_MAX]; size_t cchPath = strlen(pszArg); if (cchPath < sizeof(szPath)) { memcpy(szPath, pszArg, cchPath + 1); rcExit = rtCmdLsProcessDirectory(pOpts, hVfsDir, szPath, cchPath, &Info); } else rcExit = RTMsgErrorExitFailure("Too long argument: %s", pszArg); RTVfsDirRelease(hVfsDir); return rcExit; } /** * A /bin/ls clone. * * @returns Program exit code. * * @param cArgs The number of arguments. * @param papszArgs The argument vector. (Note that this may be * reordered, so the memory must be writable.) */ RTR3DECL(RTEXITCODE) RTFsCmdLs(unsigned cArgs, char **papszArgs) { /* * Parse the command line. */ #define OPT_AUTHOR 1000 #define OPT_BLOCK_SIZE 1001 #define OPT_COLOR 1002 #define OPT_FILE_TYPE 1003 #define OPT_FORMAT 1004 #define OPT_FULL_TIME 1005 #define OPT_GROUP_DIRECTORIES_FIRST 1006 #define OPT_SI 1007 #define OPT_DEREFERENCE_COMMAND_LINE_SYMLINK_TO_DIR 1008 #define OPT_HIDE 1009 #define OPT_INDICATOR_STYLE 1010 #define OPT_MACHINE_READABLE 1011 #define OPT_SHOW_CONTROL_CHARS 1012 #define OPT_QUOTING_STYLE 1013 #define OPT_SORT 1014 #define OPT_TIME 1015 #define OPT_TIME_STYLE 1016 static const RTGETOPTDEF s_aOptions[] = { { "--all", 'a', RTGETOPT_REQ_NOTHING }, { "--almost-all", 'A', RTGETOPT_REQ_NOTHING }, //{ "--author", OPT_AUTHOR, RTGETOPT_REQ_NOTHING }, { "--escape", 'b', RTGETOPT_REQ_NOTHING }, { "--block-size", OPT_BLOCK_SIZE, RTGETOPT_REQ_UINT32 }, { "--ctime", 'c', RTGETOPT_REQ_NOTHING }, //{ "--columns", 'C', RTGETOPT_REQ_NOTHING }, //{ "--color", OPT_COLOR, RTGETOPT_OPT_STRING }, { "--directory", 'd', RTGETOPT_REQ_NOTHING }, //{ "--dired", 'D', RTGETOPT_REQ_NOTHING }, { "--dash-f", 'f', RTGETOPT_REQ_NOTHING }, //{ "--classify", 'F', RTGETOPT_REQ_NOTHING }, //{ "--file-type", OPT_FILE_TYPE, RTGETOPT_REQ_NOTHING }, { "--format", OPT_FORMAT, RTGETOPT_REQ_STRING }, { "--full-time", OPT_FULL_TIME, RTGETOPT_REQ_NOTHING }, { "--dash-g", 'g', RTGETOPT_REQ_NOTHING }, { "--group-directories-first", OPT_GROUP_DIRECTORIES_FIRST, RTGETOPT_REQ_NOTHING }, { "--no-group", 'G', RTGETOPT_REQ_NOTHING }, { "--human-readable", 'h', RTGETOPT_REQ_NOTHING }, { "--si", OPT_SI, RTGETOPT_REQ_NOTHING }, { "--dereference-command-line", 'H', RTGETOPT_REQ_NOTHING }, { "--dereference-command-line-symlink-to-dir", OPT_DEREFERENCE_COMMAND_LINE_SYMLINK_TO_DIR, RTGETOPT_REQ_NOTHING }, //{ "--hide" OPT_HIDE, RTGETOPT_REQ_STRING }, //{ "--indicator-style" OPT_INDICATOR_STYLE, RTGETOPT_REQ_STRING }, { "--inode", 'i', RTGETOPT_REQ_NOTHING }, { "--block-size-1kib", 'k', RTGETOPT_REQ_NOTHING }, { "--long", 'l', RTGETOPT_REQ_NOTHING }, { "--dereference", 'L', RTGETOPT_REQ_NOTHING }, { "--format-commas", 'm', RTGETOPT_REQ_NOTHING }, { "--machinereadable", OPT_MACHINE_READABLE, RTGETOPT_REQ_NOTHING }, { "--machine-readable", OPT_MACHINE_READABLE, RTGETOPT_REQ_NOTHING }, { "--numeric-uid-gid", 'n', RTGETOPT_REQ_NOTHING }, { "--literal", 'N', RTGETOPT_REQ_NOTHING }, { "--long-without-group-info", 'o', RTGETOPT_REQ_NOTHING }, //{ "--indicator-style", 'p', RTGETOPT_REQ_STRING }, { "--hide-control-chars", 'q', RTGETOPT_REQ_NOTHING }, { "--show-control-chars", OPT_SHOW_CONTROL_CHARS, RTGETOPT_REQ_NOTHING }, //{ "--quote-name", 'Q', RTGETOPT_REQ_NOTHING }, //{ "--quoting-style", OPT_QUOTING_STYLE, RTGETOPT_REQ_STRING }, { "--reverse", 'r', RTGETOPT_REQ_NOTHING }, { "--recursive", 'R', RTGETOPT_REQ_NOTHING }, { "--size", 's', RTGETOPT_REQ_NOTHING }, { "--sort-by-size", 'S', RTGETOPT_REQ_NOTHING }, { "--sort", OPT_SORT, RTGETOPT_REQ_STRING }, { "--time", OPT_TIME, RTGETOPT_REQ_STRING }, { "--time-style", OPT_TIME_STYLE, RTGETOPT_REQ_STRING }, { "--sort-by-time", 't', RTGETOPT_REQ_NOTHING }, { "--tabsize", 'T', RTGETOPT_REQ_UINT8 }, { "--atime", 'u', RTGETOPT_REQ_NOTHING }, { "--unsorted", 'U', RTGETOPT_REQ_NOTHING }, { "--version-sort", 'v', RTGETOPT_REQ_NOTHING }, { "--width", 'w', RTGETOPT_REQ_UINT32 }, { "--list-by-line", 'x', RTGETOPT_REQ_NOTHING }, { "--sort-by-extension", 'X', RTGETOPT_REQ_NOTHING }, { "--one-file-per-line", '1', RTGETOPT_REQ_NOTHING }, { "--help", '?', RTGETOPT_REQ_NOTHING }, }; RTCMDLSOPTS Opts; Opts.fFollowSymlinksInDirs = false; Opts.fFollowSymlinkToAnyArgs = false; Opts.fFollowSymlinkToDirArgs = false; Opts.fFollowDirectoryArgs = true; Opts.fRecursive = false; Opts.fShowHidden = false; Opts.fShowDotAndDotDot = false; Opts.fShowBackups = true; Opts.enmSort = RTCMDLSSORT_NAME; Opts.fReverseSort = false; Opts.fGroupDirectoriesFirst = false; Opts.enmFormat = RTCMDLSFORMAT_COLS_VERTICAL; Opts.fEscapeNonGraphicChars = false; Opts.fEscapeControlChars = true; Opts.fHideControlChars = false; Opts.fHumanReadableSizes = false; /**< -h */ Opts.fSiUnits = false; Opts.cbBlock = 0; Opts.fShowOwner = true; Opts.fShowGroup = true; Opts.fNumericalIds = false; Opts.fShowINode = false; Opts.fShowAllocatedSize = false; Opts.cchTab = 8; Opts.cchWidth = 80; Opts.enmColor = RTCMDLSCOLOR_NONE; Opts.enmTime = RTCMDLSTIME_MTIME; Opts.enmTimeStyle = RTCMDLSTIMESTYLE_LOCALE; Opts.pszTimeCustom = NULL; Opts.cCollections = 0; Opts.cCollectionsAllocated = 0; Opts.papCollections = NULL; RTEXITCODE rcExit = RTEXITCODE_SUCCESS; unsigned cProcessed = 0; RTGETOPTSTATE GetState; int rc = RTGetOptInit(&GetState, cArgs, papszArgs, s_aOptions, RT_ELEMENTS(s_aOptions), 1, RTGETOPTINIT_FLAGS_OPTS_FIRST); if (RT_FAILURE(rc)) return RTMsgErrorExit(RTEXITCODE_SYNTAX, "RTGetOptInit: %Rrc", rc); for (;;) { RTGETOPTUNION ValueUnion; int chOpt = RTGetOpt(&GetState, &ValueUnion); switch (chOpt) { case 0: /* When reaching the end of arguments without having processed any files/dirs/whatever yet, we do the current directory. */ if (cProcessed > 0) { RTEXITCODE rcExit2 = rtCmdLsDisplayCollections(&Opts); if (rcExit2 != RTEXITCODE_SUCCESS) rcExit = rcExit2; rtCmdLsFreeCollections(&Opts); return rcExit; } ValueUnion.psz = "."; RT_FALL_THRU(); case VINF_GETOPT_NOT_OPTION: { RTEXITCODE rcExit2 = rtCmdLsProcessArgument(&Opts, ValueUnion.psz); if (rcExit2 != RTEXITCODE_SUCCESS) rcExit = rcExit2; cProcessed++; break; } case 'a': Opts.fShowHidden = true; Opts.fShowDotAndDotDot = true; break; case 'A': Opts.fShowHidden = true; Opts.fShowDotAndDotDot = false; break; case 'b': Opts.fEscapeNonGraphicChars = true; break; case OPT_BLOCK_SIZE: if (!ValueUnion.u32) { Assert(!Opts.papCollections); return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Invalid block size: %u", ValueUnion.u32); } Opts.cbBlock = ValueUnion.u32; Opts.fHumanReadableSizes = false; Opts.fSiUnits = false; break; case 'c': Opts.enmTime = RTCMDLSTIME_CTIME; break; case 'C': Opts.enmFormat = RTCMDLSFORMAT_COLS_VERTICAL; break; case 'd': Opts.fFollowDirectoryArgs = false; Opts.fFollowSymlinkToAnyArgs = false; Opts.fFollowSymlinkToDirArgs = false; Opts.fRecursive = false; break; case 'f': Opts.fShowHidden = true; Opts.fShowDotAndDotDot = true; if (Opts.enmFormat == RTCMDLSFORMAT_LONG) Opts.enmFormat = RTCMDLSFORMAT_COLS_VERTICAL; Opts.enmColor = RTCMDLSCOLOR_NONE; Opts.enmSort = RTCMDLSSORT_NONE; break; case OPT_FORMAT: if ( strcmp(ValueUnion.psz, "across") == 0 || strcmp(ValueUnion.psz, "horizontal") == 0) Opts.enmFormat = RTCMDLSFORMAT_COLS_HORIZONTAL; else if (strcmp(ValueUnion.psz, "commas") == 0) Opts.enmFormat = RTCMDLSFORMAT_COMMAS; else if ( strcmp(ValueUnion.psz, "long") == 0 || strcmp(ValueUnion.psz, "verbose") == 0) Opts.enmFormat = RTCMDLSFORMAT_LONG; else if (strcmp(ValueUnion.psz, "single-column") == 0) Opts.enmFormat = RTCMDLSFORMAT_SINGLE; else if (strcmp(ValueUnion.psz, "vertical") == 0) Opts.enmFormat = RTCMDLSFORMAT_COLS_VERTICAL; else if (strcmp(ValueUnion.psz, "machine-readable") == 0) Opts.enmFormat = RTCMDLSFORMAT_MACHINE_READABLE; else { Assert(!Opts.papCollections); return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Unknown format: %s", ValueUnion.psz); } break; case OPT_FULL_TIME: Opts.enmFormat = RTCMDLSFORMAT_LONG; Opts.enmTimeStyle = RTCMDLSTIMESTYLE_FULL_ISO; break; case 'g': Opts.enmFormat = RTCMDLSFORMAT_LONG; Opts.fShowOwner = false; break; case OPT_GROUP_DIRECTORIES_FIRST: Opts.fGroupDirectoriesFirst = true; break; case 'G': Opts.fShowGroup = false; break; case 'h': Opts.fHumanReadableSizes = true; Opts.fSiUnits = false; break; case OPT_SI: Opts.fHumanReadableSizes = true; Opts.fSiUnits = true; break; case 'H': Opts.fFollowSymlinkToAnyArgs = true; Opts.fFollowSymlinkToDirArgs = true; break; case OPT_DEREFERENCE_COMMAND_LINE_SYMLINK_TO_DIR: Opts.fFollowSymlinkToAnyArgs = false; Opts.fFollowSymlinkToDirArgs = true; break; case 'i': Opts.fShowINode = true; break; case 'k': Opts.cbBlock = _1K; Opts.fHumanReadableSizes = false; Opts.fSiUnits = false; break; case 'l': Opts.enmFormat = RTCMDLSFORMAT_LONG; break; case 'L': Opts.fFollowSymlinksInDirs = true; Opts.fFollowSymlinkToAnyArgs = true; Opts.fFollowSymlinkToDirArgs = true; break; case 'm': Opts.enmFormat = RTCMDLSFORMAT_COMMAS; break; case OPT_MACHINE_READABLE: Opts.enmFormat = RTCMDLSFORMAT_MACHINE_READABLE; break; case 'n': Opts.fNumericalIds = true; break; case 'N': Opts.fEscapeNonGraphicChars = false; Opts.fEscapeControlChars = false; Opts.fHideControlChars = false; break; case 'o': Opts.enmFormat = RTCMDLSFORMAT_LONG; Opts.fShowGroup = false; break; case 'q': Opts.fHideControlChars = true; break; case OPT_SHOW_CONTROL_CHARS: Opts.fHideControlChars = true; break; case 'r': Opts.fReverseSort = true; break; case 'R': Opts.fRecursive = true; break; case 's': Opts.fShowAllocatedSize = true; break; case 'S': Opts.enmSort = RTCMDLSSORT_SIZE; break; case OPT_SORT: if (strcmp(ValueUnion.psz, "none") == 0) Opts.enmSort = RTCMDLSSORT_NONE; else if (strcmp(ValueUnion.psz, "extension") == 0) Opts.enmSort = RTCMDLSSORT_EXTENSION; else if (strcmp(ValueUnion.psz, "size") == 0) Opts.enmSort = RTCMDLSSORT_SIZE; else if (strcmp(ValueUnion.psz, "time") == 0) Opts.enmSort = RTCMDLSSORT_TIME; else if (strcmp(ValueUnion.psz, "version") == 0) Opts.enmSort = RTCMDLSSORT_VERSION; else { Assert(!Opts.papCollections); return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Unknown sort by: %s", ValueUnion.psz); } break; case OPT_TIME: if ( strcmp(ValueUnion.psz, "btime") == 0 || strcmp(ValueUnion.psz, "birth") == 0) Opts.enmTime = RTCMDLSTIME_BTIME; else if ( strcmp(ValueUnion.psz, "ctime") == 0 || strcmp(ValueUnion.psz, "status") == 0) Opts.enmTime = RTCMDLSTIME_CTIME; else if ( strcmp(ValueUnion.psz, "mtime") == 0 || strcmp(ValueUnion.psz, "write") == 0 || strcmp(ValueUnion.psz, "modify") == 0) Opts.enmTime = RTCMDLSTIME_MTIME; else if ( strcmp(ValueUnion.psz, "atime") == 0 || strcmp(ValueUnion.psz, "access") == 0 || strcmp(ValueUnion.psz, "use") == 0) Opts.enmTime = RTCMDLSTIME_ATIME; else { Assert(!Opts.papCollections); return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Unknown time attribute: %s", ValueUnion.psz); } break; case OPT_TIME_STYLE: if (strcmp(ValueUnion.psz, "full-iso") == 0) Opts.enmTimeStyle = RTCMDLSTIMESTYLE_FULL_ISO; else if (strcmp(ValueUnion.psz, "long-iso") == 0) Opts.enmTimeStyle = RTCMDLSTIMESTYLE_LONG_ISO; else if (strcmp(ValueUnion.psz, "iso") == 0) Opts.enmTimeStyle = RTCMDLSTIMESTYLE_ISO; else if (strcmp(ValueUnion.psz, "locale") == 0) Opts.enmTimeStyle = RTCMDLSTIMESTYLE_LOCALE; else if (*ValueUnion.psz == '+') { Opts.enmTimeStyle = RTCMDLSTIMESTYLE_CUSTOM; Opts.pszTimeCustom = ValueUnion.psz; } else { Assert(!Opts.papCollections); return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Unknown sort by: %s", ValueUnion.psz); } break; case 't': Opts.enmSort = RTCMDLSSORT_TIME; break; case 'T': Opts.cchTab = ValueUnion.u8; break; case 'u': Opts.enmTime = RTCMDLSTIME_ATIME; break; case 'U': Opts.enmSort = RTCMDLSSORT_NONE; break; case 'v': Opts.enmSort = RTCMDLSSORT_VERSION; break; case 'w': Opts.cchWidth = ValueUnion.u32; break; case 'x': Opts.enmFormat = RTCMDLSFORMAT_COLS_HORIZONTAL; break; case 'X': Opts.enmSort = RTCMDLSSORT_EXTENSION; break; case '1': Opts.enmFormat = RTCMDLSFORMAT_SINGLE; break; case '?': { RTPrintf("Usage: to be written\n" "Options dump:\n"); for (unsigned i = 0; i < RT_ELEMENTS(s_aOptions); i++) if (s_aOptions[i].iShort < 127 && s_aOptions[i].iShort >= 0x20) RTPrintf(" -%c,%s\n", s_aOptions[i].iShort, s_aOptions[i].pszLong); else RTPrintf(" %s\n", s_aOptions[i].pszLong); #ifdef RT_OS_WINDOWS const char *pszProgNm = RTPathFilename(papszArgs[0]); RTPrintf("\n" "The path prefix '\\\\:iprtnt:\\' can be used to access the NT namespace.\n" "To list devices: %s -la \\\\:iprtnt:\\Device\n" "To list win32 devices: %s -la \\\\:iprtnt:\\GLOBAL??\n" "To list the root (hack/bug): %s -la \\\\:iprtnt:\\\n", pszProgNm, pszProgNm, pszProgNm); #endif Assert(!Opts.papCollections); return RTEXITCODE_SUCCESS; } case 'V': RTPrintf("%sr%d\n", RTBldCfgVersion(), RTBldCfgRevision()); Assert(!Opts.papCollections); return RTEXITCODE_SUCCESS; default: Assert(!Opts.papCollections); return RTGetOptPrintError(chOpt, &ValueUnion); } } }